Running Real-Time Applications in Container Environments
Running real-time applications within Docker or Podman containers on SUSE Linux Enterprise Server requires deterministic scheduling, kernel tuning and CPU shielding. Because containers share the host kernel, manually isolating real-time processes into dedicated cgroups prevents interference from background host tasks. This optimization reduces worst-case latency and helps critical workloads maintain predictable execution without thread starvation.
Prerequisites
Installed real-time kernel
Installed Docker or Podman environment
1 Understanding real-time constraints in containers #
When deploying real-time (RT) workloads within containers, it is crucial to understand the limitations of container boundaries. Containers do not provide strict enough isolation to guarantee zero cross-talk, meaning your real-time processes will inevitably be affected by background container activity.
From the perspective of the RT kernel, a container is another process. There are no container-specific settings to eliminate interference, regardless of whether it originates inside or outside a container. You should also be prepared for this interference to increase significantly if you attempt to run multiple RT applications across separate containers.
While using an RT kernel improves your worst-case latency compared to a standard SLES environment, the inherent overhead required to achieve determinism means it will not necessarily outperform a standard NOPREEMPT kernel in raw throughput. Keep these constraints in mind as you prepare and shield your containerized workloads in the following steps.
2 Running RT applications in Docker containers #
If you intend to run compute-intensive applications with real-time priority, you must make sure that kernel threads cannot starve. (This is general advice that applies to other real-time scenarios as well.)
2.1 Prerequisites #
A simple precaution is to use the rtkthread=PRIORITY and rtworkqueues=PRIORITY kernel boot parameters. Set the PRIORITY values higher than the priority of any process that has the potential to dominate a CPU. This is not strictly real-time capable, but it is safer overall.
On top of those kernel boot parameters, the following conditions must be met:
The kernel must be booted with the
nortschedcommand-line parameter to hide cgroup scheduling from the container engine.The
docker runorpodman runcommand must be issued with the--privileged=trueparameter.The container is equipped with the
chrtsystem tool.
2.2 Starting real-time application containers #
To start a container without isolation, run the command docker run or podman run using chrt to set the RT class or priority of the program you execute when starting the container. For example:
>sudodocker run --privileged=true ... /usr/bin/chrt -f 1 /usr/sbin/sshd -D
Alternatively:
>sudopodman run --privileged=true ... /usr/bin/chrt -f 1 /usr/sbin/sshd -D
The above starts sshd within the container as a SCHED_FIFO task of priority 1. If you SSH into it, any command or application run in the container inherits the scheduler's RT class and priority.
3 Shielding containers #
Currently, Docker or Podman do not provide a built-in mechanism to launch a container directly into a highly isolated CPU set (cpuset). To achieve true deterministic performance for real-time workloads, you must perform this isolation manually.
The process involves two primary phases:
Establishing the shield: Creating an isolated cgroup partition and moving all host system processes (using
systemd) away from your dedicated real-time CPUs.Migrating the container: Launching the container, and then manually migrating its processes into the newly isolated cgroup.
3.1 Establishing the shield #
The following shell script brings an isolated shield up or down. It restricts default system services to CPUs 0-3, routes interrupts away from the real-time cores, and creates an isolated partition (/sys/fs/cgroup/rtcpus) for CPUs 4-63.
#!/bin/sh
# ------------------------------------------------------------------
# Toggle script for establishing a cgroups v2 real-time shield
# ------------------------------------------------------------------
let START_CPU=4
let END_CPU=63
let ONLINE=1
let SHIELD_UP=0
GOVERNOR="performance"
# Masks for routing interrupts
DEFAULT_MASK=ffffffff,ffffffff
SHIELD_MASK=00000000,0000000f # CPUs 0-3
CGROUP2_ROOT="/sys/fs/cgroup"
SHIELD_CG="${CGROUP2_ROOT}/rtcpus"
# Capture kernel settings
if [ -f /proc/sys/kernel/sched_rt_runtime_us ]; then
RT_RUNTIME=$(cat /proc/sys/kernel/sched_rt_runtime_us)
fi
if [ -f /proc/sys/kernel/nmi_watchdog ]; then
NMI_WATCHDOG=$(cat /proc/sys/kernel/nmi_watchdog)
fi
# Detect if the shield is already active
if [ -d "$SHIELD_CG" ]; then
let SHIELD_UP=1
fi
if [ $SHIELD_UP -eq 1 ]; then
echo "Tearing down cgroup v2 shield..."
# 1. Dismantle the isolated partition
if [ -f "${SHIELD_CG}/cpuset.cpus.partition" ]; then
echo "member" > "${SHIELD_CG}/cpuset.cpus.partition" 2>/dev/null
fi
# 2. Move any leftover tasks back to the root cgroup
if [ -f "${SHIELD_CG}/cgroup.procs" ]; then
for pid in $(cat "${SHIELD_CG}/cgroup.procs" 2>/dev/null); do
echo $pid > "${CGROUP2_ROOT}/cgroup.procs" 2>/dev/null
done
fi
# 3. Remove the custom cgroup
rmdir "$SHIELD_CG" 2>/dev/null
# 4. Restore systemd slices to use all CPUs
systemctl set-property --runtime system.slice AllowedCPUs=0-63
systemctl set-property --runtime user.slice AllowedCPUs=0-63
systemctl set-property --runtime init.scope AllowedCPUs=0-63
# Restore default IRQ affinity
echo ${DEFAULT_MASK} > /proc/irq/default_smp_affinity
for irqlist in $(ls /proc/irq/*/smp_affinity 2>/dev/null); do
echo ${DEFAULT_MASK} >$irqlist 2>/dev/null
done
# Restore kernel tuning (Timers, Hugepages, Watchdogs, etc.)
[ -f /proc/sys/kernel/timer_migration ] && echo 1 > /proc/sys/kernel/timer_migration
[ -f /proc/sys/kernel/sched_rt_runtime_us ] && echo ${RT_RUNTIME} > /proc/sys/kernel/sched_rt_runtime_us
[ -f /sys/kernel/debug/tracing/tracing_on ] && echo 1 > /sys/kernel/debug/tracing/tracing_on
[ -f /sys/kernel/mm/transparent_hugepage/enabled ] && echo always > /sys/kernel/mm/transparent_hugepage/enabled
[ -f /proc/sys/kernel/nmi_watchdog ] && echo ${NMI_WATCHDOG} > /proc/sys/kernel/nmi_watchdog
[ -f /sys/devices/system/machinecheck/machinecheck0/check_interval ] && echo 300 > /sys/devices/system/machinecheck/machinecheck0/check_interval
[ -f /sys/devices/virtual/workqueue/writeback/cpumask ] && echo ${DEFAULT_MASK} > /sys/devices/virtual/workqueue/writeback/cpumask
[ -f /sys/devices/virtual/workqueue/cpumask ] && echo ${DEFAULT_MASK} > /sys/devices/virtual/workqueue/cpumask
[ -f /proc/sys/vm/stat_interval ] && echo 1 > /proc/sys/vm/stat_interval
[ -f /sys/module/processor/parameters/latency_factor ] && echo 2 > /sys/module/processor/parameters/latency_factor
[ -f /sys/module/processor/parameters/ignore_ppc ] && echo 0 > /sys/module/processor/parameters/ignore_ppc
[ -f /sys/module/processor/parameters/ignore_tpc ] && echo 0 > /sys/module/processor/parameters/ignore_tpc
if [ -f /etc/init.d/sgi_irqbalance ]; then
/etc/init.d/sgi_irqbalance start
elif systemctl is-active --quiet irqbalance; then
systemctl start irqbalance
fi
else
echo "Bringing up cgroup v2 shield..."
# Route IRQs away from shielded CPUs
if [ -f /etc/init.d/sgi_irqbalance ]; then
/etc/init.d/sgi_irqbalance stop
else
systemctl stop irqbalance 2>/dev/null
fi
echo $SHIELD_MASK > /proc/irq/default_smp_affinity
for irqlist in $(ls /proc/irq/*/smp_affinity 2>/dev/null); do
echo $SHIELD_MASK >$irqlist 2>/dev/null
done
# Apply Real-Time kernel tuning (Disable tracing, THP, watchdogs, etc.)
[ -f /proc/sys/kernel/sched_rt_runtime_us ] && echo -1 > /proc/sys/kernel/sched_rt_runtime_us
[ -f /sys/kernel/debug/tracing/tracing_on ] && echo 0 > /sys/kernel/debug/tracing/tracing_on
[ -f /sys/kernel/mm/transparent_hugepage/enabled ] && echo never > /sys/kernel/mm/transparent_hugepage/enabled
[ -f /proc/sys/kernel/nmi_watchdog ] && echo 0 > /proc/sys/kernel/nmi_watchdog
[ -f /sys/devices/system/machinecheck/machinecheck0/check_interval ] && echo 0 > /sys/devices/system/machinecheck/machinecheck0/check_interval
[ -f /sys/devices/virtual/workqueue/writeback/cpumask ] && echo ${SHIELD_MASK} > /sys/devices/virtual/workqueue/writeback/cpumask
[ -f /sys/devices/virtual/workqueue/cpumask ] && echo ${SHIELD_MASK} > /sys/devices/virtual/workqueue/cpumask
[ -f /proc/sys/vm/stat_interval ] && echo 999999 > /proc/sys/vm/stat_interval
[ -f /sys/module/processor/parameters/latency_factor ] && echo 1 > /sys/module/processor/parameters/latency_factor
[ -f /sys/module/processor/parameters/ignore_ppc ] && echo 1 > /sys/module/processor/parameters/ignore_ppc
[ -f /sys/module/processor/parameters/ignore_tpc ] && echo 1 > /sys/module/processor/parameters/ignore_tpc
# 1. Restrict default system tasks to non-shielded CPUs using systemd
systemctl set-property --runtime system.slice AllowedCPUs=0-3
systemctl set-property --runtime user.slice AllowedCPUs=0-3
systemctl set-property --runtime init.scope AllowedCPUs=0-3
# 2. Fire up the shield using native cgroups v2
mkdir -p "$SHIELD_CG"
# Ensure the cpuset controller is enabled in the subtree
echo "+cpuset" > "${CGROUP2_ROOT}/cgroup.subtree_control" 2>/dev/null || true
# Assign the real-time CPUs to our shield
echo "${START_CPU}-${END_CPU}" > "${SHIELD_CG}/cpuset.cpus"
# Set the partition to 'isolated' (disables load balancing across these CPUs)
if [ -f "${SHIELD_CG}/cpuset.cpus.partition" ]; then
echo "isolated" > "${SHIELD_CG}/cpuset.cpus.partition"
fi
# Allow time for the kernel sched_domain to rebuild
sleep 1
# Offline/online cycle to migrate timers and kthreads off shielded cores
if [ $ONLINE -eq 1 ]; then
for i in `seq ${START_CPU} ${END_CPU}`; do
echo 0 > /sys/devices/system/cpu/cpu$i/online
echo 1 > /sys/devices/system/cpu/cpu$i/online
done
# Prioritize re-initialized kthreads
systemctl restart set_kthread_prio 2>/dev/null || true
fi
if [ -f /proc/sys/kernel/timer_migration ]; then
echo 0 > /proc/sys/kernel/timer_migration
fi
GOVERNOR="performance"
fi
# Set CPUFreq scaling governor
if [ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor ]; then
CURRENT_GOVERNOR=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor)
if ! [ "$GOVERNOR" = "$CURRENT_GOVERNOR" ]; then
for i in $(ls /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null); do
echo $GOVERNOR >$i
done
fi
fi3.2 Migrating the container into the shield #
Once your rtcpus isolated cgroup is actively running, you can launch your container workload. The workflow below demonstrates how to initialize the container and seamlessly transfer its PID into the shielded partition.
The scripts below use the script created in the previous section called shield.sh. If you named it differently, please adjust the code below accordingly.
In the case of Docker, run the following script.
# 1. Execute the shield script to isolate CPUs 4-63
./shield.sh
# 2. Launch your real-time container in the background
docker_id=$(docker run -d --privileged=true my_rt_image /usr/bin/chrt -f 1 /path/to/my_app)
# 3. Extract the root Process ID (PID) of the running container
container_pid=$(docker inspect -f '{{.State.Pid}}' ${docker_id})
# 4. Migrate the container into the isolated cgroup
# Moving the root PID automatically migrates its child threads in v2
echo ${container_pid} > /sys/fs/cgroup/rtcpus/cgroup.procs
# --- Workload is now running on isolated RT CPUs ---
# 5. Clean up when finished: Stop and remove the container
docker stop ${docker_id}
docker rm ${docker_id}
# 6. Tear down the shield and restore normal system scheduling
./shield.shIn the case of Podman, run the following script:
# 1. Execute the shield script to isolate CPUs 4-63
./shield.sh
# 2. Launch your real-time container in the background (Must use sudo for Rootful mode)
# NOTE: The --annotation argument is required in cgroups v2 to allocate RT runtime budget.
podman_id=$(sudo podman run -d \
--privileged \
--annotation run.oci.cgroup.cpu.rt_runtime=950000 \
my_rt_image /usr/bin/chrt -f 1 /path/to/my_app)
# 3. Extract the root Process ID (PID) of the running Podman container
container_pid=$(sudo podman inspect -f '{{.State.Pid}}' ${podman_id})
# 4. Migrate the container into the isolated cgroup
# Moving the root PID automatically migrates its child threads in v2
echo ${container_pid} | sudo tee /sys/fs/cgroup/rtcpus/cgroup.procs
# --- Workload is now running on isolated RT CPUs (4-63) ---
# 5. Clean up when finished: Stop and remove the container
sudo podman stop ${podman_id}
sudo podman rm ${podman_id}
# 6. Tear down the shield and restore normal system scheduling
./shield.sh
By writing the container's root PID into the cgroup.procs file of the shielded cgroup, the unified hierarchy automatically tracks and confines all spawned child threads to your designated real-time CPUs.
4 Legal Notice #
Copyright© 2006– 2026 SUSE LLC and contributors. All rights reserved.
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or (at your option) version 1.3; with the Invariant Section being this copyright notice and license. A copy of the license version 1.2 is included in the section entitled “GNU Free Documentation License”.
For SUSE trademarks, see https://www.suse.com/company/legal/. All other third-party trademarks are the property of their respective owners. Trademark symbols (®, ™ etc.) denote trademarks of SUSE and its affiliates. Asterisks (*) denote third-party trademarks.
All information found in this book has been compiled with utmost attention to detail. However, this does not guarantee complete accuracy. Neither SUSE LLC, its affiliates, the authors, nor the translators shall be held liable for possible errors or the consequences thereof.