01

Prerequisites

Fedora 44, Podman 5.x rootless, the C++ toolchain (GCC 14 / Clang 18, Conan 2, CMake, Ninja), supporting tools (hey, jq, libabigail, bpftrace), and the host-check script that confirms everything is wired correctly before you touch the demos.

⏱ 15 minutes (+ install)Section 1

This section is the gate. None of the demos will run cleanly if your host isn’t set up correctly, and the failure modes when something is missing are the kind that swallow an hour of debugging time before you realize the problem is your environment.

The good news: on a fresh Fedora 44 install, the entire prerequisite list is one dnf install and one gh configuration step. This section walks you through it deliberately, says what each piece is for, and ends with a host-check script that verifies everything works before you commit to running a demo.

Diagram

Diagram 01-prerequisites-toolchain hasn't been drawn yet. See diagrams/README.md for the conventions.

What gets installed where: Fedora packages, toolchain, Conan, Podman storage. Download .excalidraw

Why Fedora 44

Three reasons we picked it as the baseline, in decreasing order of importance:

  1. Kernel ≥ 6.8 with io_uring multishot accept and recv enabled by default. Demo 3 uses these primitives directly; older kernels either don’t have them or require kernel module loading we’d rather not document.
  2. Podman 5.x in the default repo, rootless out of the box, with cgroups v2 delegation on by default. Demos 2, 5, and 6 all rely on rootless cgroup writability for memory.high, cpu.weight, and cpuset.cpus. Some hardened distros disable this and require a manual systemctl --user setup.
  3. gcc-toolset-14 and clang 18 are both available and current. We use both — GCC for the UBI-based demos (matches what Red Hat ships in gcc-toolset-14), Clang for the PGO build (better profile-data tooling and Sampling-PGO support).

No subscription needed. Every UBI 9 image we use comes from registry.access.redhat.com/ubi9/... and is freely pullable and redistributable. You don’t need a Red Hat subscription for any of the demos. Subscription-only images like ubi9/toolbox are not in use.

Other distros that should work with minor adjustments:

Distro Verdict
RHEL 9 / CentOS Stream 9 Most demos work. Kernel may lag on io_uring multishot — check uname -r ≥ 5.19.
Ubuntu 24.04 LTS Most demos work; package names differ throughout. The host-check script flags these.
Arch / openSUSE Tumbleweed Should work. We don’t test against them, but kernel and toolchain are recent enough.
macOS (any) Container parts work via podman machine. Kernel-feature demos (2, 3, 5) won’t.
WSL2 (any) Container parts work. Cgroup v2 delegation is unreliable; demo 5 in particular flakes.

If you’re on a distro not listed, the host-check script at the bottom of this section will tell you what’s missing.

What you need installed

The full list, with what each piece is for. Installation commands come right after.

Container runtime and supporting tools

  • podman (≥ 5.0) — the OCI runtime. Rootless by default.
  • podman-compose — for the multi-service stacks in demos 3 and 4.
  • buildah — sometimes needed when podman build hits its limits; we don’t use it directly but the demos shell out to it in one place.
  • skopeo — only needed if you want to inspect remote image manifests. Not strictly required, but very useful.

C++ toolchain

  • gcc-c++ — Fedora 44 ships GCC 14.x as the default g++, which has full C++23 support; that’s all you need on the host. (UBI-based container builds use gcc-toolset-14 separately, but that’s installed inside the container Image, not on your host.)
  • clang, clang-tools-extra, lld, llvm — the gating Clang. Used for PGO instrumentation and clang-tidy in demo 6.
  • cmake (≥ 3.25) — needed for CMake presets v6.
  • ninja-build — the build driver of choice; faster and cleaner than make for our build sizes.
  • python3-pip — for installing Conan 2.
  • conan (≥ 2.0, ≤ 3.0) — installed via pip, not dnf.

Quality / debugging tools

  • gdb, gdb-gdbserver — for the demo 6 debug sidecar.
  • cppcheck — first-pass static analysis.
  • libabigail — provides abidiff for the demo 6 ABI check.
  • bpftrace, bcc-tools — for the §9 profiling layer.
  • perf (linux-tools on some distros) — CPU sampling.

Load generation and convenience

  • hey — HTTP load generator. Not in dnf; we install via Go or a binary download.
  • ghz — gRPC load generator. Optional; demo 3 falls back to hey if ghz isn’t on PATH.
  • jq — JSON parsing in shell scripts. Universal.
  • curl — every script uses it.
  • bc — needed by one helper for byte-formatting.

Installation, one command at a time

1. Update the system

sudo dnf update -y

2. Install everything via dnf in one batch

sudo dnf install -y \
    podman podman-compose buildah skopeo \
    gcc-c++ \
    clang clang-tools-extra lld llvm \
    cmake ninja-build \
    python3-pip golang \
    gdb gdb-gdbserver \
    cppcheck \
    libabigail \
    bpftrace bcc-tools \
    perf \
    jq curl bc \
    git

This pulls roughly 800 MB of packages, mostly toolchain. Coffee break.

golang is included because we use go install to fetch hey and (optionally) ghz — see step 4 below. If you already have a Go toolchain, the dnf install will no-op on it.

3. Install Conan 2 via pip

We pin Conan to the 2.x line because Conan 1 and Conan 2 have incompatible CLIs and our lockfiles target 2.x:

pip install --user 'conan>=2.0,<3.0'

Verify:

~/.local/bin/conan --version    # should print "Conan version 2.x.y"

If ~/.local/bin isn’t on your PATH, add it:

echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Initialize a default Conan profile so the demos have something to build against:

conan profile detect --force

This writes ~/.conan2/profiles/default with sensible defaults inferred from your toolchain. You can edit it later; for now the detected profile is fine.

4. Install hey

hey is a small Go program; the canonical install is via go install. The previous AWS S3 binary distribution is no longer publicly readable (returns HTTP 403), so don’t follow tutorials that point at it.

go install github.com/rakyll/hey@latest

Make sure Go’s bin directory is on your PATH:

# bash:
echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# zsh:
echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

# verify
hash -r
which hey                  # → /home/<you>/go/bin/hey
hey -h | head -3           # → "Usage: hey [options...] <url>"

If go install is slow or unreachable, the from-source build is the fallback:

mkdir -p /tmp/hey-build && cd /tmp/hey-build
git clone --depth 1 https://github.com/rakyll/hey.git .
go build -o hey .
sudo install -m 0755 hey /usr/local/bin/hey
cd && rm -rf /tmp/hey-build
hey -h | head -3

5. (Optional) Install ghz for gRPC load testing

Demo 3 uses ghz if it’s available and skips the gRPC bench otherwise. If you want full demo 3 output:

go install github.com/bojand/ghz/cmd/ghz@latest

6. Enable lingering if running rootless on a server

If your Fedora 44 host is a server you don’t sit at a console for, podman’s rootless cgroup delegation only works while you’re logged in. Enable lingering so it works whether or not you’re at a TTY:

loginctl enable-linger "$USER"

7. Verify rootless cgroup v2 delegation

The crucial check that distinguishes “this will work” from “you’ll debug obscure errors for two hours”:

cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.subtree_control

You should see cpu cpuset io memory pids (or a superset). If you see nothing, or only some of those, the delegation isn’t enabled. The shipped tutorial has a helper script that automates this:

# from the repo root
./scripts/cgroup-delegation.sh check     # show current state
./scripts/cgroup-delegation.sh enable    # install drop-in (uses sudo)
# log out and back in (or reboot) for it to activate
./scripts/cgroup-delegation.sh verify    # confirm

If you’d rather do it by hand (the script is doing exactly this):

sudo mkdir -p /etc/systemd/system/user@.service.d
cat <<'EOF' | sudo tee /etc/systemd/system/user@.service.d/delegate.conf
[Service]
Delegate=cpu cpuset io memory pids
EOF
sudo systemctl daemon-reload
# log out and back in, or reboot for the user manager to pick this up

The most important controllers for our demos:

  • cpu — needed for --cgroup-conf=cpu.weight=N and --cpus (demo 5 weighted scenario). Note that --cpu-weight is not a real podman flag, despite mirroring the cgroup file name — use --cgroup-conf=cpu.weight=N to set the v2 weight directly.
  • cpuset — needed for --cpuset-cpus (demo 5 pinned scenario). An earlier version of this guide claimed cpuset worked without delegation; that turned out to be wrong on Fedora 44 with podman 5.x — the --cpuset-cpus flag triggers a “controller cpuset is not available” error from crun without delegation.
  • memory — needed for --memory and memory.high (demos 2, 6)
  • io — needed for I/O limits (not heavily used by current demos but cheap to include)
  • pids — already delegated by default

Re-run the cat command after re-login to verify all five appear.

Verify your kernel has what the demos need

Demo 3’s io_uring work uses IORING_OP_RECV_MULTISHOT, which landed in kernel 5.19. Demo 5’s cpuset.cpus writability needs cgroup v2. Check both:

echo "Kernel:  $(uname -r)"
echo "Cgroups: $(stat -fc %T /sys/fs/cgroup)"   # should print "cgroup2fs"

The kernel version should be ≥ 5.19; on Fedora 44 you’ll see something like 6.8.x or 6.10.x, well past the requirement.

Set your gh (GitHub CLI) authentication

Several demos use the gh CLI for repo and Pages operations, and the deployment workflow does too:

sudo dnf install -y gh
gh auth login                # follow the interactive prompts
gh auth status               # confirm scopes include repo, workflow

If your token doesn’t have the workflow scope, add it now — the deploy workflow won’t push to gh-pages without it:

gh auth refresh -h github.com -s repo,workflow,admin:repo_hook

Configure registry access

The UBI-based images pull from registry.access.redhat.com. Anonymous pull works for ubi9/ubi and ubi9/ubi-minimal, but if you hit a rate limit (you might, on a cold network), authenticate:

podman login registry.redhat.io
# enter the credentials from access.redhat.com → Service Accounts

The Grafana / Prometheus / Mimir / Tempo / Loki images come from Docker Hub. Anonymous pull works there too, but you may want to authenticate to bypass anonymous rate limits:

podman login docker.io

When docker.io is unreachable

A minority of corporate networks (and a few home setups with ad-blocking DNS) block docker.io outright. The host-check script will flag this with a [fail] docker.io (hub) unreachable line.

By project policy (see CONTRIBUTING.md) all our container images come from Red Hat UBI, and Prometheus routes through Quay (quay.io/prometheus/prometheus). Demos 1, 2, 3, 5, and 6 will run fine without Docker Hub reachability.

The one image affected is docker.io/grafana/otel-lgtm, which our observability stack uses for demo 4. If Docker Hub is blocked, demo 4 won’t run unless you mirror the image.

The podman save | podman load air-gap workaround:

# On a reachable jump host:
podman pull docker.io/grafana/otel-lgtm:0.8.1
podman save docker.io/grafana/otel-lgtm:0.8.1 -o lgtm.tar

# Transfer lgtm.tar to your build host (scp, USB, whatever), then:
podman load -i lgtm.tar

If a mirror isn’t viable, demo 4 simply won’t run — but everything else will.

Clone this repo

mkdir -p ~/Dev
cd ~/Dev
git clone https://github.com/patterncatalyst/cpp-container-optimization-tutorial.git
cd cpp-container-optimization-tutorial

The repo lays out as:

cpp-container-optimization-tutorial/
├── _docs/                     # this tutorial's prose
├── _plans/                    # reconciliation plan
├── examples/                  # seven runnable demos
│   ├── demo-01-image-strategy/
│   ├── demo-02-stl-layout/
│   └── ...
├── observability/             # Grafana/Prom/Mimir/Tempo/Loki stack
├── scripts/                   # helpers + per-demo test scripts
└── diagrams/                  # paired SVG + .excalidraw files

Run the host-check script

A short script that exercises every prerequisite and prints a clear PASS / FAIL line for each. Run this before you touch any demo.

./scripts/check-host.sh

Expected output on a correctly-set-up Fedora 44 box (your registry warnings may vary depending on whether your network reaches quay.io and docker.io):

[ ok ]  fedora baseline                  Fedora Linux 44 (Workstation Edition)
[ ok ]  kernel >= 5.19                   6.10.7-200.fc44.x86_64
[ ok ]  cgroup v2                        cgroup2fs
[ ok ]  rootless cgroup delegation       cpu cpuset io memory pids
[ ok ]  podman >= 5.0                    5.2.1
[ ok ]  cmake >= 3.25                    3.28.2
[ ok ]  podman-compose                   /usr/bin/podman-compose
[ ok ]  buildah                          /usr/bin/buildah
[ ok ]  skopeo                           /usr/bin/skopeo
[ ok ]  ninja                            /usr/bin/ninja
[ ok ]  jq                               /usr/bin/jq
[ ok ]  curl                             /usr/bin/curl
[ ok ]  bc                               /usr/bin/bc
[ ok ]  git                              /usr/bin/git
[ ok ]  gh                               /usr/bin/gh
[ ok ]  hey                              /home/<you>/go/bin/hey
[ ok ]  g++ >= 14                        14.2.1
[ ok ]  clang >= 18                      18.1.6
[ ok ]  conan 2.x                        2.5.0
[ ok ]  cppcheck                         2.13.0
[ ok ]  abidiff                          2.4.0
[ ok ]  bpftrace                         0.21.0
[ ok ]  gdb                              14.2
[ ok ]  registry.access.redhat.com       reachable
[warn]  quay.io                          unreachable
        ↳ Affects: future use; not currently required.
[warn]  docker.io (hub)                  unreachable
        ↳ Affects: demo-01 build, demo-04 runtime.

All 24 required checks passed.
2 optional check(s) flagged warnings — review the table to see
which demos are affected.

Required checks (the 24) gate the script’s exit code. Warnings don’t — they’re informational, telling you which demos may be constrained by your network.

If any line says FAIL, scroll up — the script prints the exact remediation command for each failure.

Note: the host-check script is added in this section’s commit; if you cloned an older revision and the file isn’t there yet, the bash version is short enough to inline below. We’re keeping it in scripts/ so it stays runnable outside the tutorial flow.

Pre-pull and verify-stacks

Two convenience scripts under scripts/ that save you debugging time:

./scripts/pre-pull.sh                 # pulls every image referenced by the project
./scripts/verify-stacks.sh            # smoke-tests the shared observability stack
./scripts/verify-stacks.sh --quick    # skip slow stacks (e.g. observability)

Run ./scripts/pre-pull.sh once after first clone — it warms the local image cache so subsequent demo runs start in seconds instead of minutes. Especially important if you’ll be presenting; you want the network surprises to happen now, not in front of an audience.

Run ./scripts/verify-stacks.sh whenever you think something might have broken — after a system update, after pulling fresh images, or before walking on stage. Each stack passes if it can up, respond to a health endpoint, and down cleanly.

The verify script intentionally only tests shared infrastructure (currently just the observability stack). Per-demo verification lives in scripts/test-demo-NN-*.sh — run those when you’re walking through a specific demo’s section.

If any pull fails, the message will say which image and why — usually either Docker Hub being throttled (re-run later) or a network / firewall blocking the registry (see “When docker.io is unreachable” above for the workaround).

Common things that go wrong

A short list, with fixes:

podman build fails with error creating overlay mount: permission denied. Your ~/.local/share/containers/storage is on a filesystem (often NFS) that doesn’t support overlayfs. Move storage to a local disk:

mkdir -p /var/tmp/containers-$(id -u)
podman system reset --force
cat <<EOF > ~/.config/containers/storage.conf
[storage]
driver = "overlay"
graphroot = "/var/tmp/containers-$(id -u)"
EOF

hey reports Get "...": dial tcp: lookup ...: i/o timeout. Your container is listening on the wrong interface. Demos pin listeners to 0.0.0.0 and bind via -p 127.0.0.1:PORT:8080; if yours doesn’t, you’ll see this. The demo source pins to 0.0.0.0 intentionally; if you’ve modified it, check your edit.

cmake --preset release says “preset not found”. You’re on CMake < 3.25. Our presets use schema v6, which requires 3.25 or newer. Check cmake --version and upgrade.

abidiff not found despite dnf install libabigail. On Fedora 44 the binary is in /usr/bin/abidiff; if your PATH is unusual, point at it explicitly. The demo’s run-clang-tidy wrapper handles this; if it doesn’t, file an issue.

Rootless cgroup write returns Permission denied or controller cpuset is not available. Delegation isn’t enabled or didn’t stick. Run: ./scripts/cgroup-delegation.sh check to diagnose, then ./scripts/cgroup-delegation.sh enable to fix. After enabling, you must log out and back in (or reboot) for the user manager to pick it up. The --cpuset-cpus error in particular needs the cpuset controller specifically — see G-40 in the gotcha catalog.

What’s next

You have a working environment. Open §2 — Introduction & mental model to read the why of this tutorial: what about containers actually changes how C++ performance work plays out, and the four-layer model that frames the rest of the sections.

If you’d rather skip ahead and confirm the toolchain works by running something concrete first, jump to §3 — Container strategy and run Demo 1. You can come back for §2 once the cmake/podman pipeline has stopped feeling like a black box.