Skip to main content
RunBook Academy

Docker & ContainersXVI Β· PerformanceeBPF

eBPF β€” an introduction for container operators

Advanced⏱ ~24 mindockerbpftrace

What you'll learn

  • Explain what an eBPF program is and what the verifier guarantees
  • State the kernel version and privilege requirements honestly
  • Attribute an eBPF observation to a specific container via its cgroup ID
  • Recognise which container tools are eBPF-based and what each one is for
  • Know when eBPF is the wrong tool

Prerequisites

Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12

Not yet marked complete on this device.

eBPF lets you load a small sandboxed program into the running kernel, attach it to a hook, and have it observe or act on events β€” without a kernel module, without a reboot, and without modifying the application being observed.

For containers that is unusually valuable, because a container is a process on a kernel you already control. You do not need to get a tracing agent into the image, negotiate with the application team, or restart anything.

This lesson is an introduction. It is deliberately honest about what it costs to use, because most eBPF material is not.

What an eBPF program is

A user-space loader compiles a program to eBPF bytecode and submits it via the bpf(2) syscall. Before the kernel will run it, the verifier proves statically that the program:

  • terminates β€” no unbounded loops;
  • reads and writes only memory it is permitted to touch, with every pointer dereference preceded by a bounds check the verifier can see;
  • calls only the helper functions allowed for its program type;
  • stays within instruction and stack limits.

If the proof succeeds, the program is JIT-compiled to native code and attached to a hook. It then runs in kernel context, at kernel speed, with a static guarantee that it cannot crash the kernel or read arbitrary memory. That guarantee is what makes it acceptable to run on a production host, and it is the entire reason eBPF exists rather than everyone writing kernel modules.

Programs communicate with user space through maps β€” typed key-value structures the kernel program writes and the loader reads. Attach points include kprobes (any kernel function), tracepoints (stable kernel events), uprobes (user-space functions), perf events, network tc and XDP, and the LSM hooks used by security tooling.

The requirements, stated honestly

Read-only / Safecan this host run eBPF tooling?
echo "kernel:    $(uname -r)"

# BTF: the difference between portable tools and needing kernel headers.
[ -r /sys/kernel/btf/vmlinux ] \
&& echo 'BTF:       present' \
|| echo 'BTF:       MISSING - CO-RE tools will not load'

# Unprivileged loading policy. 2 means disabled and not re-enableable at runtime.
echo "unpriv_bpf_disabled: $(sysctl -n kernel.unprivileged_bpf_disabled 2>/dev/null || echo 'n/a')"

# Kernel lockdown blocks several program types under Secure Boot.
echo "lockdown:  $(cat /sys/kernel/security/lockdown 2>/dev/null || echo 'not enabled')"

# Anything already loaded?
sudo bpftool prog show 2>/dev/null | head -10 || echo 'bpftool not installed'

The structural problem: eBPF does not know what a container is

This is the part that matters most for this course and the part most introductions skip entirely.

The kernel has no concept of a container. eBPF programs see PIDs in the initial PID namespace, so a probe that reports pid 48213 gives you a number that does not exist inside the container and does not appear in docker ps. Correlating an observation to a container is your problem.

The reliable handle is the cgroup ID, because cgroups are exactly the kernel object Docker creates per container.

Read-only / Saferesolve a container to its cgroup path
CONTAINER=api
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
CGROUP=$(sed -n 's/^0:://p' /proc/"$PID"/cgroup)
CGPATH="/sys/fs/cgroup$CGROUP"

echo "container: $CONTAINER"
echo "host pid:  $PID"
echo "cgroup:    $CGPATH"

bpftrace provides both halves of the correlation: a cgroup builtin holding the cgroup ID of the process that triggered the probe, and a cgroupid() function that resolves a cgroup v2 path to that ID at compile time.

Service impact possibletrace one container only
CONTAINER=api
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
CGPATH="/sys/fs/cgroup$(sed -n 's/^0:://p' /proc/"$PID"/cgroup)"

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve
/cgroup == cgroupid(str($1))/
{
printf("%-8d %-16s %s\n", pid, comm, str(args->filename));
}' "$CGPATH"
Read-only / Safescoped exec trace
$ sudo bpftrace -e '...' /sys/fs/cgroup/system.slice/docker-....scope
Attaching 1 probe...
48213    sh               /bin/sh
48219    curl             /usr/bin/curl
48244    sh               /bin/sh
48245    nc               /usr/bin/nc

Illustrative output

A shell and a nc spawning inside a service container is the classic post-exploitation signature, and this is roughly what Falco and Tracee productise: the same probes, plus rule evaluation, plus the container metadata lookup done for you.

What people actually use it for

ToolWhat it doesAttach points
bpftraceAd-hoc one-liners and short scripts. The exploratory tool.kprobes, tracepoints, uprobes
BCC tools (execsnoop, opensnoop, biolatency, tcplife)Prebuilt, focused answers to common questionsmixed
CiliumContainer networking and policy in eBPF instead of iptablestc, XDP, socket
FalcoRuntime security rules over syscall streamstracepoints, modern BPF driver
TraceeRuntime security and forensics eventstracepoints, LSM
Pixie, Parca, PyroscopeContinuous profiling and auto-instrumentationperf events, uprobes

Three one-liners worth knowing, each answering a question that is genuinely hard otherwise:

Service impact possiblethree useful probes
# 1. Block I/O latency as a histogram, host-wide. Answers 'is the disk slow?'
sudo bpftrace -e '
tracepoint:block:block_rq_issue { @start[args->dev, args->sector] = nsecs; }
tracepoint:block:block_rq_complete
/@start[args->dev, args->sector]/
{
@usecs = hist((nsecs - @start[args->dev, args->sector]) / 1000);
delete(@start[args->dev, args->sector]);
}
interval:s:30 { exit(); }'

# 2. Which files a container opens, by cgroup. Answers 'what config is it reading?'
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_openat
/cgroup == cgroupid(str($1))/
{ @[str(args->filename)] = count(); }
interval:s:20 { exit(); }' /sys/fs/cgroup/system.slice/docker-CONTAINER_ID.scope

# 3. Count syscalls by name for one cgroup. The seccomp-profile starting point.
sudo bpftrace -e '
tracepoint:raw_syscalls:sys_enter
/cgroup == cgroupid(str($1))/
{ @[ksym(*(kaddr("sys_call_table") + args->id * 8))] = count(); }
interval:s:20 { exit(); }' /sys/fs/cgroup/system.slice/docker-CONTAINER_ID.scope

The third one is worth flagging: reading sys_call_table is architecture- and kernel-specific and will not work everywhere. It is included because syscall enumeration is the real starting point for building a seccomp profile, and because it illustrates the general fragility of kprobe-style tracing β€” you are attaching to kernel internals that carry no stability guarantee.

When eBPF is the wrong tool

Reach for something simpler when:

  • A cgroup file already answers it. Throttling, memory usage, per-container I/O and OOM kills are all in /sys/fs/cgroup. No tracing required, no overhead, no privileges beyond reading a file.
  • The application can be asked. An HTTP request rate is a metrics endpoint, not a uprobe. Instrumented is better than inferred whenever it exists.
  • You need history. eBPF observes live events. It cannot tell you what happened at 02:00 last night unless something was already running and recording.
  • The host cannot support it. No BTF, locked-down kernel, or a managed platform that does not grant the capabilities. Find out before you build a plan around it.
  • The probe cost is not justified. A printf on a high-frequency tracepoint on a loaded host is real overhead. Aggregate in the kernel, scope by cgroup, and bound the run with an interval and exit().

Knowledge check

Knowledge check Β· 5 questions

  1. Q1. An eBPF program is:

  2. Q2. A bpftrace probe reports pid 48213, but that PID does not appear inside the container. Why?

  3. Q3. Why filter by cgroup inside the eBPF program rather than piping the output through grep?

  4. Q4. Which conditions can prevent modern eBPF tooling from running on a host? Select all that apply.

  5. Q5. To find out whether a container is being CPU-throttled, eBPF is the appropriate tool.

Passing score: 75%. Answers are checked in this browser.