Docker & ContainersXVI Β· PerformanceeBPF
eBPF β an introduction for container operators
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
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
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.
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.
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"$ sudo bpftrace -e '...' /sys/fs/cgroup/system.slice/docker-....scopeAttaching 1 probe...
48213 sh /bin/sh
48219 curl /usr/bin/curl
48244 sh /bin/sh
48245 nc /usr/bin/ncIllustrative 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
| Tool | What it does | Attach points |
|---|---|---|
bpftrace | Ad-hoc one-liners and short scripts. The exploratory tool. | kprobes, tracepoints, uprobes |
BCC tools (execsnoop, opensnoop, biolatency, tcplife) | Prebuilt, focused answers to common questions | mixed |
| Cilium | Container networking and policy in eBPF instead of iptables | tc, XDP, socket |
| Falco | Runtime security rules over syscall streams | tracepoints, modern BPF driver |
| Tracee | Runtime security and forensics events | tracepoints, LSM |
| Pixie, Parca, Pyroscope | Continuous profiling and auto-instrumentation | perf events, uprobes |
Three one-liners worth knowing, each answering a question that is genuinely hard otherwise:
# 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.scopeThe 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
printfon a high-frequency tracepoint on a loaded host is real overhead. Aggregate in the kernel, scope by cgroup, and bound the run with anintervalandexit().
Knowledge check
Knowledge check Β· 5 questions
Q1. An eBPF program is:
Q2. A bpftrace probe reports pid 48213, but that PID does not appear inside the container. Why?
Q3. Why filter by cgroup inside the eBPF program rather than piping the output through grep?
Q4. Which conditions can prevent modern eBPF tooling from running on a host? Select all that apply.
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.