You’ve got a container that can’t reach a downstream service. Your first move is docker exec -it <container> netstat -tuln. Instead you get OCI runtime exec failed: exec: "netstat": executable file not found in $PATH. Annoying, but not surprising. Alpine, Debian-slim, and distroless images strip this stuff out on purpose, and it happens constantly. This tutorial covers both fixes: attach a throwaway debug container to the target’s network namespace, or bake the tools permanently into your image.
We’ll walk through both approaches on Docker and Kubernetes. We’ll also sort out whether you actually want netstat or ss on a modern base image, and land on a clear default for production versus development.
Why Minimal Images Don’t Ship With netstat, ss, or tcpdump
Alpine Linux, Debian-slim, Ubuntu-slim, and distroless base images follow one rule: ship only what the app needs to run. Some skip the shell entirely. Distroless skips the package manager too. None of them carry a full networking toolkit.
There are two reasons for this, and both matter:
- Image size. Alpine’s base image runs 5–8 MB compressed. Add
net-tools,iproute2, andtcpdumpwith their dependencies, and you tack on another 10–25 MB. That can double or triple the image size for tools you only need once something’s already broken. - Attack surface.
tcpdumpneeds raw socket access (CAP_NET_RAW).netstatandssexpose process-to-socket mappings, useful to you, but just as useful to an attacker who’s already gained a foothold. Every binary you add is one more thing to exploit, patch, or flag as a CVE hit in your scan.
Distroless images push this further. There’s no shell (sh/bash) and no package manager at all. docker exec into one won’t even give you a prompt to install anything from. That’s by design, not an oversight.
Prerequisites
Before you start, make sure you have:
- Docker Engine 24.x or later (Docker Desktop 4.3x+ on Windows/macOS, or Docker Engine on Linux)
- A running container to debug (any image, such as Alpine, Debian-slim, or distroless)
- Basic comfort with the command line and container networking concepts
kubectl1.30+ installed and configured, if you’re debugging Kubernetes pods (optional)- Root or sudo access on the Docker host, if you plan to use
--cap-addor host networking
| Requirement | Details |
|---|---|
| Docker Engine | 24.x or later (tested on 27.x) |
| Host OS | Linux (Ubuntu 24.04 used in examples), Windows Server, or macOS via Docker Desktop |
| kubectl | 1.30+ (optional, for Kubernetes debugging) |
| Disk space | ~200 MB free for pulling debug images |
Step-by-Step Guide
Step 1: Confirm the Tools Are Actually Missing
Before reaching for a fix, verify what you’re dealing with. Exec into the target container and try each tool:
docker exec -it my-app sh -c "netstat -tuln || echo 'netstat missing'; ss -tuln || echo 'ss missing'; tcpdump --version || echo 'tcpdump missing'"
Expected output on a typical Alpine 3.20 base:
sh: netstat: not found
netstat missing
sh: ss: not found
ss missing
sh: tcpdump: not found
tcpdump missing
If the container has no shell at all (OCI runtime exec failed: exec: "sh": executable file not found), you’re looking at a distroless or scratch-based image. Skip straight to Step 2. There’s nothing to install into anyway.
Step 2: Attach an Ephemeral Debug Container (Docker)
Default to this approach for anything running in production. Instead of modifying the target image, you start a separate container with a full toolset. It shares the target’s network namespace using --network container:<id>.
First, get the target container’s ID or name:
docker ps --filter "name=my-app" --format "{{.ID}} {{.Names}}"
a3f9c21d8b4e my-app
Now attach a debug container to that exact network namespace. nicolaka/netshoot is a popular troubleshooting image. It bundles netstat, ss, tcpdump, curl, dig, and more:
docker run -it --rm \
--network container:a3f9c21d8b4e \
--cap-add NET_RAW \
--cap-add NET_ADMIN \
nicolaka/netshoot
--network container:a3f9c21d8b4e: joins the new container to the target’s network namespace. It sees the same interfaces, IP addresses, and open sockets--cap-add NET_RAW --cap-add NET_ADMIN: grants the raw-socket capabilitiestcpdumpneeds for packet capture. Most runtimes drop these by default--rm: removes the debug container automatically when you exit, so it never lingers on the host
You’re now inside a fully-equipped container, sitting on the target’s network stack. Try:
ss -tuln
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*
tcp LISTEN 0 128 [::]:8080 [::]:*
Then capture live traffic on the shared interface:
tcpdump -i eth0 -n port 8080
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
14:32:07.881223 IP 172.18.0.5.52344 > 172.18.0.3.8080: Flags [S], seq 891234
That’s the SYN packet hitting port 8080. It’s the exact connection attempt you were chasing.
When you’re done, exit the debug container (Ctrl+D or exit). Thanks to --rm, it’s gone immediately. Your target image stays untouched.
Tip: If
nicolaka/netshootfeels heavy for a quick check, trywbitt/network-multitoolormodem7/network-multitoolinstead. Both are lighter Alpine-based alternatives, roughly 16 MB compressed, withminimalandextratags. Theextratag addsssandtsharkon top ofnetstatandtcpdump.
Step 3: Attach a Debug Container to a Kubernetes Pod
On Kubernetes, you don’t have direct access to docker run. Pods are frequently distroless with no shell at all. kubectl debug solves this. It injects an ephemeral container into the pod’s namespaces without redeploying anything. No restart, no downtime, no explaining to your team why the pod bounced.
Find the pod you want to debug:
kubectl get pods -l app=my-app
NAME READY STATUS RESTARTS AGE
my-app-7d4f9c8b6-x2kpl 1/1 Running 0 2h
Attach a debug container that shares the target container’s network namespace:
kubectl debug -it pod/my-app-7d4f9c8b6-x2kpl \
--image=nicolaka/netshoot \
--target=my-app \
-- /bin/bash
--target=my-app: shares the process and network namespace of themy-appcontainer inside the pod. Tools in the debug container see its sockets and processes--image=nicolaka/netshoot: the ephemeral container’s image. Swap inwbitt/network-multitoolif you prefer a lighter footprint-it ... -- /bin/bash: opens an interactive shell inside the new ephemeral container once it starts
Expected output:
Targeting container “my-app”. If you don’t see processes from this container it may be because the container runtime doesn’t support this feature.
Defaulting debug container name to debugger-8h2kd.
You’ll land in a shell inside the ephemeral container, sharing the target’s network namespace. From there, ss -tuln and tcpdump -i eth0 work exactly as they did in the Docker example.
When you exit, the ephemeral container terminates. It stays visible in kubectl describe pod for a short time, for audit purposes. It never touches the pod’s spec or restarts your application container.
Tip: If your cluster runs an older
kubectl(below 1.23),kubectl debugmay not be available. Upgrade the client; the feature’s been stable since Kubernetes 1.23.
Step 4: Permanently Add Tools on Alpine (Dockerfile)
For dev or staging images, baking the tools in beats reaching for a sidecar every time. You’re iterating quickly anyway, and a slightly larger image doesn’t matter.
Edit your Dockerfile:
# Dockerfile
FROM alpine:3.20
# Install network debugging tools — dev/staging only, not for production
RUN apk add --no-cache \
net-tools \
iproute2 \
tcpdump \
busybox-extras
net-tools: provides the classicnetstatbinaryiproute2: providesss. On Alpine,ssis bundled with the standardiproute2package rather than a separate subpackagetcpdump: packet capture utilitybusybox-extras: adds a lightweight built-innetstat/topif you want to skipnet-toolsentirely and shave a few MB
Build and check the size difference:
docker build -t my-app:debug-tools .
docker images my-app --format "table {{.Tag}}\t{{.Size}}"
TAG SIZE
debug-tools 31.4MB
Compare that to a bare alpine:3.20 base (around 7.8 MB). The tools alone add roughly 20+ MB, mostly from tcpdump‘s dependency on libpcap.
Step 5: Permanently Add Tools on Debian/Ubuntu-slim (Dockerfile)
The equivalent on a Debian- or Ubuntu-based slim image uses apt-get:
# Dockerfile
FROM debian:12-slim
# Install network debugging tools — dev/staging only, not for production
RUN apt-get update && \
apt-get install -y --no-install-recommends \
net-tools \
iproute2 \
tcpdump && \
rm -rf /var/lib/apt/lists/*
net-tools: providesnetstat,ifconfig,routeiproute2: providesssandip. Unlike Alpine, Debian hasn’t split this package, so one install gets you both--no-install-recommends: skips optional recommended packages you don’t strictly need, keeping the added size downrm -rf /var/lib/apt/lists/*: clears the apt package index cache after install. Left alone, it adds several MB of dead weight to the image layer
Build and check:
docker build -t my-app:debian-debug .
docker images my-app --format "table {{.Tag}}\t{{.Size}}"
TAG SIZE
debian-debug 118MB
debian:12-slim starts heavier than Alpine, around 74 MB base. Adding the same three tools brings it to roughly 118 MB. That’s a smaller relative jump than Alpine’s, but still real weight in absolute terms.
Step 6: Verify Inside the Running Container
Whichever path you took, confirm the tools actually work before you call it done.
docker run --rm -it my-app:debug-tools sh -c "ss -h | head -3 && netstat --version && tcpdump --version"
Usage: ss [ OPTIONS ] [ FILTER ]
-h, –help this message
-V, –version output version information
net-tools 2.10-alpine
tcpdump version 4.99.4
If all three report version info instead of “not found,” you’re set.
Configuration: Which Tool Should You Actually Install?
This is where a lot of guides get sloppy. netstat and ss aren’t interchangeable. Only one of them is actively maintained.
| Tool | Package | Alpine | Debian-slim | Status |
|---|---|---|---|---|
netstat | net-tools | Installable via apk add net-tools | Installable via apt-get install net-tools | Deprecated upstream; still widely used out of habit |
ss | iproute2 (Alpine and Debian) | Installable via apk add iproute2 | Bundled with iproute2 | Actively maintained; the modern default |
tcpdump | tcpdump | Installable via apk add tcpdump | Installable via apt-get install tcpdump | Actively maintained on both |
BusyBox netstat | busybox-extras | Built into Alpine’s BusyBox applet set | Not applicable (Debian doesn’t ship BusyBox) | Lightweight but limited flag support |
The short answer: reach for ss over netstat on any base built after 2020. net-tools, which provides netstat, has seen no meaningful upstream development in years. iproute2, which provides ss, is actively maintained by the same kernel networking team that ships ip. ss also runs faster. It reads directly from the kernel’s netlink interface instead of parsing /proc/net/tcp.
That said, netstat‘s output format is the one a lot of admins have memorized. For a quick sanity check, either tool works. Muscle memory is a legitimate reason to keep net-tools around in a dev image, but don’t assume it’s there by default.
Image Size and Attack Surface Tradeoffs
| Approach | Size Impact | Attack Surface | Best For |
|---|---|---|---|
| Ephemeral debug sidecar | Zero: target image never changes | Zero added to production image; tools only exist during the debug session | Production, staging |
| Permanent install (Alpine) | +20–25 MB typical | Tools + libpcap present in every deployed container, always | Dev/local iteration |
| Permanent install (Debian-slim) | +25–45 MB typical | Same: tools ship in every container instance | Dev/local iteration |
| Prebuilt multitool image as sidecar | Zero to target; sidecar itself is ~16–60 MB depending on image | None added to target; sidecar is disposable | Ad hoc troubleshooting across many containers |
Recommendation
Default to the ephemeral sidecar approach for anything that ships to production. It costs nothing in image size and adds zero permanent attack surface. It also works against distroless images that have no shell to install into anyway. Reserve permanent installs for images that never leave your dev or CI environment, where instant docker exec access to ss and tcpdump outweighs the size cost.
Leaving tcpdump permanently installed in a production image is a bad habit worth breaking. Its presence tells anyone who gains container access that packet capture is one command away. And CAP_NET_RAW is a capability you’d otherwise drop entirely in a hardened runtime profile.
Platform Notes
Windows
Install Docker Desktop for Windows (WSL2 backend recommended) and run all commands from PowerShell or Windows Terminal. The workflow is identical to Linux, since Docker Desktop runs a Linux VM under the hood.
docker version
Client: Docker Engine – Community
Version: 27.3.1
Server: Docker Desktop 4.34.2 (167172)
Engine:
Version: 27.3.1
If you’re debugging Kubernetes pods, install kubectl via winget install -e --id Kubernetes.kubectl. Point it at your cluster’s context before running kubectl debug.
macOS
Install Docker Desktop for Mac. Pick the Apple Silicon or Intel build matching your hardware. Then open Terminal and confirm the engine is running:
docker version
Client: Docker Engine – Community
Version: 27.3.1
Server: Docker Desktop 4.34.2 (167172)
Engine:
Version: 27.3.1
Install kubectl via Homebrew if you’re debugging Kubernetes pods:
brew install kubectl
kubectl version --client
Web (Docker Hub Reference)
You don’t need a browser to run any of the commands here. But Docker Hub is worth a quick check, to confirm image sizes and available tags before you pull something new.
Check the Tags tab on any image page to compare compressed sizes across variants, say, alpine:3.20 vs alpine:3.20-musl. Decide which base to build your debug or production image from before you commit.
Tips and Troubleshooting
“command not found” after installing the package: Double-check you’re using the right package name for your base. On Alpine, ss comes from the iproute2 package, not the old iprout2 typo, and not a separate iproute2-ss package. Run apk add --no-cache iproute2 and verify with ss -h.
tcpdump installed but captures nothing: The container is almost certainly missing CAP_NET_RAW and CAP_NET_ADMIN. Add them explicitly:
docker run -it --rm --cap-add NET_RAW --cap-add NET_ADMIN --network container:a3f9c21d8b4e nicolaka/netshoot tcpdump -i eth0
If that’s still not enough (say, in a heavily locked-down runtime), fall back to --network host for the debug session only, never for the target container itself.
Debug container shows different network activity than the target: You forgot --network container:<id>. The debug container started on its own default bridge network instead. Confirm with:
docker inspect a3f9c21d8b4e --format '{{.NetworkSettings.IPAddress}}'
docker inspect <debug-container-id> --format '{{.NetworkSettings.IPAddress}}'
If the IPs differ, the debug container isn’t sharing the namespace. Rerun with the correct --network container:<id> flag.
Distroless target has no shell at all: You can’t docker exec or install anything inside it, full stop. This is intentional hardening. Your only option is an external debug container or pod. Attach it via --network container:<id> on Docker, or kubectl debug --target=<container> on Kubernetes.
Image size ballooned more than expected on Alpine: tcpdump pulls in libpcap, and net-tools pulls in a handful of shared libraries. Run docker history my-app:debug-tools to see which layer added the weight. Consider busybox-extras instead of net-tools if you only need basic netstat output.
Wrapping Up
Ephemeral debug containers give you the same ss, netstat, and tcpdump output as a permanent install. They don’t leave a single extra byte or capability in your production image. That’s the approach worth defaulting to; it’s also the only one that works against distroless targets at all. Save permanent installs for dev and staging Dockerfiles, where convenience matters more than shipped attack surface.
| Step | Action | Applies To |
|---|---|---|
| 1 | Verify tools are missing with docker exec | Any minimal image |
| 2 | Attach debug container via --network container:<id> | Docker, production |
| 3 | Use kubectl debug --target=<container> | Kubernetes, production |
| 4 | apk add net-tools iproute2 tcpdump | Alpine, dev/staging only |
| 5 | apt-get install net-tools iproute2 tcpdump | Debian/Ubuntu-slim, dev/staging only |
| 6 | Verify with ss -h, netstat --version, tcpdump --version | Any approach |
Resources
- Docker CLI Reference
- Alpine Linux Wiki: Configure Networking
- Official Alpine Docker Image on Docker Hub
- Official BusyBox Docker Image on Docker Hub
- Alpine iproute2 package details
- wbitt/network-multitool on Docker Hub
- modem7/network-multitool on Docker Hub
- alpinelinux/docker-alpine GitHub repository