56.7 Debug slim and distroless containers: docker debug and netshoot
Why: the problem
Suppose we are at PhonePe. Our payment service runs on a distroless image, as chapter 55 taught us. Now it cannot reach the database, and we try docker exec -it pay sh. The answer: executable file not found. There is no shell, no curl, no ping, no nslookup. That is good for security and bad for debugging at 2 am. We must not add debug tools to the production image; that would undo the hardening. Instead we bring the tools to the container from outside. Two ways: docker debug, which attaches a toolbox shell to any container or image without changing it, and nicolaka/netshoot, a network toolbox image that we start inside the same network or PID namespace as the broken container. The same idea exists in Kubernetes as kubectl debug with ephemeral containers. In this lesson we debug a distroless service's DNS, ports, TLS and processes without touching its image.
What: every term in plain English
- Distroless — Image with only the app and runtime; no shell or package manager.
- docker debug — Docker CLI command that opens a toolbox shell into any container or image; tools are not added to it.
- netshoot — nicolaka/netshoot: image with curl, dig, tcpdump, ss, iperf3, mtr, openssl and more.
- --network container:NAME — Join another container's network namespace: same IP, ports and DNS.
- --pid container:NAME — See another container's processes.
- Sidecar — A helper container placed next to the main one.
- tcpdump — Captures packets on an interface.
- ss — Shows listening sockets and connections.
- kubectl debug — Kubernetes equivalent using ephemeral containers.
How: step by step
- Confirm there is no shell. docker exec fails.
- Try docker debug. Toolbox shell inside the container's view.
- Start netshoot in its network. --network container:pay.
- Check DNS, ports, TLS. dig, ss, curl, openssl.
- Check processes. --pid container:pay.
- Capture traffic if needed. tcpdump to a file.
- Remove the toolbox. --rm; the target image is unchanged.
Example 1: A distroless service that we cannot exec into
Goal. A distroless service that we cannot exec into.
compose.yaml
services:
pay:
image: gcr.io/distroless/python3-debian12:nonroot
command: ["-m", "http.server", "8080"]
environment:
DB_HOST: postgres-db
networks: [backend]
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: change-me
networks: [backend]
networks:
backend: {}
Commands
docker compose up -d
docker compose exec pay sh
docker compose exec pay ls
docker inspect -f '{{.Config.User}} {{json .Config.Entrypoint}}' $(docker compose ps -q pay)
What each line does
distroless python3 nonroot— Python runtime only, runs as UID 65532.DB_HOST: postgres-db— A deliberate bug: the service is called db.exec sh— Fails: no shell exists.exec ls— Fails too: no ls.inspect— We can still read config from outside.
Green output
OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
OCI runtime exec failed: exec failed: unable to start container process: exec: "ls": executable file not found in $PATH
nonroot ["/usr/bin/python3.11"]
Expected result. This is the normal situation with hardened images. We need tools from outside.
Example 2: netshoot in the same network namespace finds the DNS bug
Goal. netshoot in the same network namespace finds the DNS bug.
Commands
PAY=$(docker compose ps -q pay)
docker run --rm -it --network container:$PAY nicolaka/netshoot:v0.13
# inside netshoot:
ss -ltnp
dig +short postgres-db
dig +short db
nc -zv db 5432
curl -s -o /dev/null -w '%{http_code}\n' localhost:8080
exit
docker run --rm --network container:$PAY nicolaka/netshoot:v0.13 \
tcpdump -i eth0 -c 20 -w /dev/stdout port 5432 > db.pcap
What each line does
--network container:$PAY— netshoot shares pay's IP, ports and DNS resolver.ss -ltnp— pay listens on 8080, so the app is up.dig postgres-db— Empty: no such name. That is the bug.dig db— Resolves: the right name is db.nc -zv db 5432— Port reachable.tcpdump to db.pcap— Capture for Wireshark if deeper analysis is needed.
Green output
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 5 0.0.0.0:8080 0.0.0.0:*
(no output for postgres-db)
172.22.0.2
Connection to db (172.22.0.2) 5432 port [tcp/postgresql] succeeded!
200
Expected result. Two minutes of netshoot found it: DB_HOST points to a name that does not exist. Fix DB_HOST: db and redeploy. The pay image never changed.
Example 3: docker debug and a PID-namespace toolbox for processes and files
Goal. docker debug and a PID-namespace toolbox for processes and files.
Commands
docker debug $(docker compose ps -q pay)
# inside the debug shell (tools come from the toolbox, files are the container's):
ps aux
cat /proc/1/environ | tr '\0' '\n' | grep DB_
ls -l /usr/lib/python3.11 | head -3
exit
docker run --rm -it --pid container:$(docker compose ps -q pay) --network container:$(docker compose ps -q pay) \
nicolaka/netshoot:v0.13 sh -c 'ps aux; ls /proc/1/root/ | head; cat /proc/1/root/etc/os-release | head -2'
docker debug gcr.io/distroless/python3-debian12:nonroot
What each line does
docker debug CONTAINER— Opens a shell with tools, seeing the container's filesystem and processes./proc/1/environ— Real environment of PID 1: confirms DB_HOST.--pid container:— netshoot sees pay's processes./proc/1/root— The target container's root filesystem, from the toolbox.docker debug IMAGE— Explore an image that is not running.Exit— Toolbox disappears; the container is unchanged.
Green output
PID USER COMMAND
1 nonroot /usr/bin/python3.11 -m http.server 8080
DB_HOST=postgres-db
PID USER TIME COMMAND
1 65532 0:00 /usr/bin/python3.11 -m http.server 8080
bin boot dev etc home lib ...
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
Expected result. Both tools let us read env, processes and files of a shell-less container. docker debug is easiest when available; netshoot with --pid and --network works on any Docker host.
Every command, step by step (plain docker and Compose)
-
Ports listening
docker run --rm --network container:pay nicolaka/netshoot:v0.13 ss -ltnWhat it does: Sockets in pay's namespace.
LISTEN 0.0.0.0:8080 -
DNS
docker run --rm --network container:pay nicolaka/netshoot:v0.13 dig +short dbWhat it does: Name resolution as pay sees it.
172.22.0.2 -
HTTP
docker run --rm --network container:pay nicolaka/netshoot:v0.13 curl -sv http://db:5432What it does: Raw HTTP test.
Connected to db -
TLS
docker run --rm nicolaka/netshoot:v0.13 openssl s_client -connect api.razorpay.com:443 -briefWhat it does: Certificate check.
Protocol version: TLSv1.3 -
Path
docker run --rm nicolaka/netshoot:v0.13 mtr -rwc 5 8.8.8.8What it does: Route and loss.
HOST: ... Loss% 0.0 -
Host network
docker run --rm --network host nicolaka/netshoot:v0.13 ss -ltnWhat it does: Host sockets.
LISTEN 0.0.0.0:22 -
Kubernetes
kubectl debug -it pay-7d9f --image=nicolaka/netshoot --target=payWhat it does: Ephemeral debug container.
Defaulting debug container name to debugger-x2k
When it is red 1
docker: 'debug' is not a docker command.
Cause. docker debug is a Docker Desktop and paid-plan feature.
Fix, step by step
- Update Docker Desktop and sign in, or use netshoot with --pid and --network container:.
When it is red 2
docker: Error response from daemon: cannot join network of a non running container
Cause. The target container has stopped.
Fix, step by step
- docker logs it first.
- Run the image with --entrypoint override using a debug tag, or docker debug IMAGE.
When it is red 3
tcpdump: eth0: You don't have permission to capture on that device
Cause. Missing NET_RAW / NET_ADMIN capability.
Fix, step by step
- Add --cap-add NET_ADMIN --cap-add NET_RAW to the netshoot container only.
Practice tasks
Easy Task 1: No shell
Why: Context.
What to do: Confirm exec sh fails.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
docker compose exec pay shEasy Task 2: netshoot
Why: Toolbox.
What to do: Start netshoot in pay's network.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
docker run --rm -it --network container:PAY nicolaka/netshoot:v0.13Medium Task 3: Listening
Why: Ports.
What to do: Run ss -ltnp.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
ss -ltnpMedium Task 4: DNS
Why: Bug.
What to do: Find the wrong DB_HOST with dig.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
dig +short postgres-dbMedium Task 5: Port test
Why: TCP.
What to do: nc -zv db 5432.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
nc -zv db 5432Hard Task 6: PID share
Why: Processes.
What to do: See PID 1 of pay.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
--pid container:PAY ... ps auxHard Task 7: Env
Why: Config.
What to do: Read /proc/1/environ.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
tr '\0' '\n' with /proc/1/environHard Task 8: tcpdump
Why: Packets.
What to do: Capture 20 packets.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
tcpdump -i eth0 -c 20 -w /dev/stdoutMedium Task 9: docker debug
Why: Desktop.
What to do: Debug the image.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
docker debug gcr.io/distroless/python3-debian12:nonrootEasy Task 10: Fix
Why: Done.
What to do: Set DB_HOST: db and redeploy.
- Open a terminal with Docker 24 or newer.
- docker debug needs Docker Desktop 4.33+ or a Docker Pro, Team or Business plan; netshoot works everywhere.
- Run each step and compare with the solution.
Solution
docker compose up -dHomework: try at home
At home, deploy a distroless service with a wrong env variable, find the bug only with netshoot, then repeat with docker debug if you have it.
Recap
- Hardened images have no shell, by design.
- Bring tools from outside: docker debug or netshoot.
- --network container: shares IP, ports and DNS.
- --pid container: shares processes; /proc/1/root shows files.
- Give NET_ADMIN only to the toolbox.
- In Kubernetes use kubectl debug.
FAQ
Q. Is docker debug free?
It ships with Docker Desktop; check the current plan terms in Docker's docs.
Q. Is netshoot safe to run in production?
Run it briefly, with --rm, and only on hosts you own; it has powerful tools.
Q. Can I debug a crashed container?
Not its namespaces; run the image again with docker debug IMAGE or a debug tag.
Q. What is busybox:debug?
Distroless ships :debug tags with a busybox shell for non-production use.
Q. Does this work in ECS?
Use ECS Exec, or add a netshoot sidecar to the task definition temporarily.
Q. Can I copy files out of distroless?
Yes, docker cp works without a shell.
Q. Which tools inside netshoot matter most?
dig and nslookup for DNS, ss for ports, curl for HTTP, nc for raw TCP, openssl for TLS, tcpdump for packets, iperf3 for bandwidth.
Interview questions
-
How do you debug a container with no shell?
Model answer: docker debug, or a netshoot container sharing its network and PID namespaces, or kubectl debug in Kubernetes.
-
What does --network container:x do?
Model answer: Puts the new container in x's network namespace: same interfaces, IP, ports and resolver.
-
Why keep debug tools out of images?
Model answer: Smaller attack surface and fewer CVEs.
-
How would you capture traffic of one container?
Model answer: tcpdump from netshoot in its network namespace with NET_RAW.
-
What are ephemeral containers?
Model answer: Kubernetes debug containers added to a running pod without restart.
-
How do you read a distroless container's env?
Model answer: docker inspect, or /proc/1/environ from a PID-sharing toolbox.
Quick quiz
Q1. Why not add curl to the prod image?
It undoes hardening.
Q2. Flag to share a container's network?
--network container:NAME.
Q3. Flag to see its processes?
--pid container:NAME.
Q4. Where is the target's filesystem from a PID-sharing toolbox?
/proc/1/root.
Q5. Kubernetes equivalent?
kubectl debug with an ephemeral container.
Samajla ka?
Revision card
Interview card