Skip to content

Capture Docker container traffic with tcpdump and Wireshark

Capture a Docker container's traffic without installing anything in it: tcpdump in a helper container, a .pcap file, nsenter, and a live pipe into Wireshark.

Published on 5 min read

This guide shows four ways to capture a Docker container's traffic without touching its image: tcpdump in a helper container that shares its network namespace, a .pcap file you open later, nsenter from the host, and a live stream into Wireshark. All commands and outputs come from a run on Docker 29 with Wireshark 4.6 on macOS.

What you'll build

 blogtest-cap-client  ----  blogtest-cap-net  ----  blogtest-cap-web (nginx)
    (alpine, wget)          (Docker bridge)             eth0  <-- capture here
                                                         ^
                                       helper: netshoot, --net container:blogtest-cap-web

A small nginx server and a client on one Docker network. You capture on the web server's eth0, but from outside its image.

What you need

  • Docker Engine on Linux, or Docker Desktop on macOS or Windows.
  • The nicolaka/netshoot image, which includes tcpdump, tshark and nsenter.
  • Wireshark on your machine for the last two steps.

Step 1: start a web server and a client

docker network create blogtest-cap-net
docker run -d --name blogtest-cap-web --network blogtest-cap-net nginx:1.27-alpine
docker run -d --name blogtest-cap-client --network blogtest-cap-net alpine sleep infinity

Most images are minimal. The nginx image has no tcpdump:

docker exec blogtest-cap-web tcpdump -i eth0
OCI runtime exec failed: exec failed: unable to start container process: exec: "tcpdump": executable file not found in $PATH

You could install it, but that changes the container you are debugging. A helper container is cleaner.

Step 2: tcpdump from a helper container

Every container has its own network namespace: its own interfaces, routes and firewall rules. --net container:<name> starts a new container inside an existing container's namespace instead of a new one. The helper brings the tools; the target brings the network.

docker run --rm -it --net container:blogtest-cap-web nicolaka/netshoot \
  tcpdump -i eth0 -nn tcp port 80

In a second terminal, make a request from the client:

docker exec blogtest-cap-client wget -qO- http://blogtest-cap-web/

The helper prints the full exchange, from the TCP handshake to the HTTP response:

listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
00:36:12.227377 IP 172.18.0.3.55768 > 172.18.0.2.80: Flags [S], seq 3855742255, win 64240, options [mss 1460,sackOK,TS val 107695869 ecr 0,nop,wscale 10], length 0
00:36:12.227389 IP 172.18.0.2.80 > 172.18.0.3.55768: Flags [S.], seq 3870663783, ack 3855742256, win 65160, options [mss 1460,sackOK,TS val 934273704 ecr 107695869,nop,wscale 10], length 0
00:36:12.227406 IP 172.18.0.3.55768 > 172.18.0.2.80: Flags [.], ack 1, win 63, options [nop,nop,TS val 107695869 ecr 934273704], length 0
00:36:12.227494 IP 172.18.0.3.55768 > 172.18.0.2.80: Flags [P.], seq 1:93, ack 1, win 63, options [nop,nop,TS val 107695869 ecr 934273704], length 92: HTTP: GET / HTTP/1.1
00:36:12.229035 IP 172.18.0.2.80 > 172.18.0.3.55768: Flags [P.], seq 1:234, ack 93, win 64, options [nop,nop,TS val 934273706 ecr 107695869], length 233: HTTP: HTTP/1.1 200 OK

No --privileged and no --cap-add. tcpdump needs a raw packet socket, and NET_RAW is in Docker's default capability set. -nn turns off name and port resolution, which keeps the output fast and honest.

The helper sees everything the target sees, including traffic to other containers and to the internet. Change the filter to udp port 53 to watch DNS lookups, or drop it to see all.

Step 3: save a .pcap file

Mount a host folder and write raw packets with -w. Here the helper runs detached so you can stop it when you are done:

docker run -d --name blogtest-cap-dump --net container:blogtest-cap-web \
  -v "$PWD":/cap nicolaka/netshoot \
  tcpdump -i eth0 -w /cap/web.pcap tcp port 80
sleep 2
for i in 1 2 3; do docker exec blogtest-cap-client wget -qO /dev/null http://blogtest-cap-web/; done
sleep 2
docker stop blogtest-cap-dump
docker logs blogtest-cap-dump
docker rm blogtest-cap-dump
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
36 packets captured
36 packets received by filter
0 packets dropped by kernel

docker stop sends SIGTERM, and tcpdump writes its file before it exits. Three requests gave 36 packets: 12 per HTTP exchange. Keep the pauses. The first sleep lets tcpdump start before the traffic. The second matters more: when I stopped the helper right after the requests, the log said 0 packets captured and 37 packets received by filter, and the file was empty. The kernel had handed the packets over, but tcpdump had not yet processed them when it was stopped. On a Linux host the file belongs to root, because tcpdump ran as root in the container; on Docker Desktop for Mac it belonged to my user.

Check and read the file with the command-line tools that come with Wireshark:

capinfos web.pcap
tshark -r web.pcap -Y http
File type:           Wireshark/tcpdump/... - pcap
File encapsulation:  Ethernet
Number of packets:   36
File size:           5844 bytes
Capture duration:    0.184401 seconds

    4   0.000114   172.18.0.3 → 172.18.0.2   HTTP 158 GET / HTTP/1.1
    8   0.000600   172.18.0.2 → 172.18.0.3   HTTP 681 HTTP/1.1 200 OK  (text/html)
   16   0.090476   172.18.0.3 → 172.18.0.2   HTTP 158 GET / HTTP/1.1
   20   0.090873   172.18.0.2 → 172.18.0.3   HTTP 681 HTTP/1.1 200 OK  (text/html)
   28   0.183770   172.18.0.3 → 172.18.0.2   HTTP 158 GET / HTTP/1.1
   32   0.184114   172.18.0.2 → 172.18.0.3   HTTP 681 HTTP/1.1 200 OK  (text/html)

Or open web.pcap in Wireshark with File > Open. On macOS, capinfos and tshark live in /Applications/Wireshark.app/Contents/MacOS/ if they are not on your PATH.

Step 4: nsenter from the host

nsenter runs a program inside another process's namespaces (nsenter(1)). With -n it enters only the network namespace, so you use the host's own tcpdump on the container's interfaces. On a Linux host:

PID=$(docker inspect -f '{{.State.Pid}}' blogtest-cap-web)
sudo nsenter -t "$PID" -n tcpdump -i eth0 -nn tcp port 80

On Docker Desktop the PID belongs to the Linux VM, not to macOS or Windows. Run nsenter from a privileged container that shares the VM's process namespace instead. This is the form I tested:

PID=$(docker inspect -f '{{.State.Pid}}' blogtest-cap-web)
docker run --rm --privileged --pid host nicolaka/netshoot \
  nsenter -t "$PID" -n tcpdump -i eth0 -nn -c 4 tcp port 80
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
00:36:55.792594 IP 172.18.0.3.36866 > 172.18.0.2.80: Flags [S], seq 3626622323, win 64240, options [mss 1460,sackOK,TS val 1205746555 ecr 0,nop,wscale 10], length 0
00:36:55.792609 IP 172.18.0.2.80 > 172.18.0.3.36866: Flags [S.], seq 1320152520, ack 3626622324, win 65160, options [mss 1460,sackOK,TS val 2242168658 ecr 1205746555,nop,wscale 10], length 0
00:36:55.792626 IP 172.18.0.3.36866 > 172.18.0.2.80: Flags [.], ack 1, win 63, options [nop,nop,TS val 1205746555 ecr 2242168658], length 0
00:36:55.792689 IP 172.18.0.3.36866 > 172.18.0.2.80: Flags [P.], seq 1:93, ack 1, win 63, options [nop,nop,TS val 1205746555 ecr 2242168658], length 92: HTTP: GET / HTTP/1.1
4 packets captured

Use nsenter when you want the host's tools or when you can't pull an image. Otherwise the helper container from step 2 is simpler and needs no privileges.

You can also capture one level up, on the Docker bridge itself, to see every container on that network at once. A user-defined network's bridge is named br- plus the first 12 characters of the network ID:

BR=br-$(docker network inspect -f '{{.Id}}' blogtest-cap-net | cut -c1-12)
docker run --rm --net host nicolaka/netshoot tcpdump -i "$BR" -nn tcp port 80

That worked on Docker Desktop too, from a --net host container that sees the VM's interfaces.

Step 5: stream live into Wireshark

tcpdump can write pcap to standard output with -w -, and -U flushes each packet as soon as it is captured instead of when the buffer fills (tcpdump(1)). Wireshark reads a capture from standard input with -i -, and -k starts capturing at once (wireshark(1)). Join them with a pipe.

On Linux:

docker run --rm --net container:blogtest-cap-web nicolaka/netshoot \
  tcpdump -i eth0 -U -w - tcp port 80 | wireshark -k -i -

On macOS:

docker run --rm --net container:blogtest-cap-web nicolaka/netshoot \
  tcpdump -i eth0 -U -w - tcp port 80 \
  | /Applications/Wireshark.app/Contents/MacOS/Wireshark -k -i -

Wireshark opens and packets appear as they happen. To test the pipe without the GUI, I replaced Wireshark with tshark -i - and made one request:

Capturing on 'Standard input'
    1   0.000000   172.18.0.3 → 172.18.0.2   TCP 74 33202 → 80 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=3917340435 TSecr=0 WS=1024
    2   0.000022   172.18.0.2 → 172.18.0.3   TCP 74 80 → 33202 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=575054274 TSecr=3917340435 WS=1024
    3   0.000053   172.18.0.3 → 172.18.0.2   TCP 66 33202 → 80 [ACK] Seq=1 Ack=1 Win=64512 Len=0 TSval=3917340435 TSecr=575054274
    4   0.000112   172.18.0.3 → 172.18.0.2   HTTP 158 GET / HTTP/1.1
    8   0.001163   172.18.0.2 → 172.18.0.3   HTTP 681 HTTP/1.1 200 OK  (text/html)

Two details matter here:

  • No -t. With docker run -t, Docker puts a terminal between tcpdump and the pipe, and the binary stream gets mangled. In my test tshark stopped at once with Frame 1 too long (1936288800 bytes) and captured 0 packets.
  • Keep -U. Without it, tcpdump buffers output on a pipe and Wireshark may show nothing for a long time on a quiet link.

To capture a remote Docker host, put ssh user@host in front of the docker run part. The pcap stream travels over SSH and Wireshark still reads standard input.

Which method to use

MethodNeedsBest for
Helper with --net container:Nothing beyond DockerMost captures, on any host
.pcap file via a mounted folderA writable host folderSaving evidence, sharing, later analysis
nsenterRoot on the host, or a privileged --pid host containerUsing host tools, no extra image
Docker bridge br-...Host network accessSeeing every container on one network
Pipe into WiresharkWireshark on your machine, no -tInteractive debugging as it happens

Clean up

docker rm -f blogtest-cap-web blogtest-cap-client
docker network rm blogtest-cap-net
rm -f web.pcap

Going further

  • Capture the 802.1Q trunk in the VLAN lab and expand the VLAN header in Wireshark.
  • Capture on both sides of the firewall in the DMZ lab to see DNAT rewrite the destination address.
  • Use a ring buffer for long captures: tcpdump -i eth0 -w /cap/web.pcap -C 10 -W 5 rotates through five files, web.pcap0 to web.pcap4, of 10 million bytes each.
  • Capturing in GNS3 instead? See GNS3 Wireshark capture not working.

Packet capture in Isothrum

Isothrum's editor is free in the browser, with no account, and its engine runs locally as WebAssembly. You draw a lab of containers from any Docker image, VMs and routers, and export Docker Compose and other files to run it yourself, so the helper-container method above works on any lab it exports. A desktop app is coming soon that will run labs locally with consoles, live status and packet capture straight into Wireshark.

Related articles

GNS3 packet capture won't start, Wireshark errors out or shows no packets. Check the running node, the capture command, the Wireshark path and permissions.
Build an eBGP lab with three FRR routers in Docker, one per AS. Bring up sessions, exchange prefixes, read the AS path and filter a route.
Set up an isolated security lab with DVWA as a target, an attacker box and a packet-capture sensor in Docker. Tested, with legal and isolation rules.