Tips and Tricks
We run bootc in production across all of our infrastructure. These tips come from problems we've hit and solved along the way.
Ship containers with your image using bound images
If your host runs containers via Quadlet, the default behavior is for Podman to pull them on first boot. That means your services wait on a network pull before they can start — and if the pull fails, they don't start at all.
Logically bound images solve this. When bootc pulls a host update, it
also pulls any bound images into read-only storage, so they're available
immediately at boot with no separate podman pull.
Three pieces wire up a bound image:
1. The .image file — declares the OCI reference. Place it in
/usr/share/containers/systemd/:
# /usr/share/containers/systemd/my-app.image
[Image]
Image=docker.io/org/my-app:v1.2.3
2. A symlink in bound-images.d — tells bootc to pull this image
when deploying the OS. Without the symlink, bootc ignores the .image file:
RUN install -d -m 0755 /usr/lib/bootc/bound-images.d \
&& ln -s ../../../share/containers/systemd/my-app.image \
/usr/lib/bootc/bound-images.d/my-app.image
3. The .container quadlet — the systemd unit that runs the container.
Add GlobalArgs=--storage-opt additionalimagestore=/usr/lib/bootc/storage
so Podman can find the image in bootc's read-only store:
# /usr/share/containers/systemd/my-app.container
[Container]
Image=docker.io/org/my-app:v1.2.3
GlobalArgs=--storage-opt additionalimagestore=/usr/lib/bootc/storage
The Image= in both files must match exactly.
Containerfile pattern
COPY my-app.image /usr/share/containers/systemd/my-app.image
RUN install -d -m 0755 /usr/lib/bootc/bound-images.d \
&& ln -s ../../../share/containers/systemd/my-app.image \
/usr/lib/bootc/bound-images.d/my-app.image
COPY my-app.container /usr/share/containers/systemd/my-app.container
Tips
- Pin your image tags in the
.imagefile — never use:latest. When you update the version, update both the.imageand.containerfiles. - Use bound images for anything integral to the host — services that must be present at first boot with no network dependency. Use normal Quadlet auto-pull for containers that can tolerate a pull delay or that change independently of the host image.
Check that NetworkManager isn't timing out on unused NICs
This is especially common on bare-metal hosts with multiple NICs.
After a reboot, services that depend on network-online.target can take
45–60+ seconds to start. If your services are slow to come up after a
reboot, NetworkManager might be the culprit.
What happens
NetworkManager-wait-online.service blocks network-online.target until
every managed interface has finished activation. On bare-metal hosts with
multiple NICs, a secondary NIC may have link but no DHCP server.
NetworkManager auto-activates a profile on it, the DHCP transaction times
out after 45 seconds, and everything downstream waits.
How to check
# Look for interfaces stuck in "connecting" or "disconnected"
nmcli device status
# See how long wait-online took
systemctl status NetworkManager-wait-online.service --no-pager
# Check for DHCP timeouts
journalctl -u NetworkManager.service | grep -iE 'dhcp|fail|timeout'
Signs of the problem:
- An interface in
connectingstate that isn't your primary uplink. dhcp4 (<iface>): activation: beginning transaction (timeout in 45 seconds)on a NIC with no DHCP server.
How to fix
Tell NetworkManager to ignore the unused NIC:
# Replace IFACE with the interface name (e.g. eno2, enp2s0f1np1)
printf '[keyfile]\nunmanaged-devices=interface-name:IFACE\n' \
| sudo tee /etc/NetworkManager/conf.d/99-unmanaged-IFACE.conf
sudo systemctl restart NetworkManager
Verify it worked:
nmcli device status # IFACE should show "unmanaged"
sudo systemctl restart NetworkManager-wait-online # should finish in <5s
This is a per-host fix because NIC names differ across hardware. Don't bake it into the image unless all your hosts share the same NIC layout.