How to Restrict Docker Container Resource Usage
What You’ll Need
- A cloud server running Linux, such as a Hetzner VPS or DigitalOcean droplet
- Docker Engine installed (version 20.10 or higher recommended)
- Docker Compose V2 installed for multi-container configurations
- A domain name managed via Namecheap if you are setting up reverse proxies or SSL certificates
Table of Contents
- Why Docker Resource Limits Are Critical
- Restricting CPU Allocations
- Restricting Memory and Swap Allocation
- Controlling Disk I/O Limits
- Implementing Resource Limits in Docker Compose
- Monitoring Container Resource Usage
- Getting Started
Why Docker Resource Limits Are Critical
By default, a Docker container has no resource constraints. It can access all CPU cycles, memory, and disk I/O exposed by the host operating system’s kernel. In a development environment, this flexibility is convenient. In a production environment on a Hetzner VPS, unconstrained containers represent a severe liability.
When a single container experiences a memory leak, a runaway loop, or a sudden traffic spike, it can consume 100 percent of host RAM or CPU resources. The Linux kernel responds to memory exhaustion by invoking the Out-Of-Memory (OOM) Killer. The OOM Killer inspects running processes and forcefully terminates them to free system RAM. Without explicit Docker resource limits, the kernel might terminate critical host services like the SSH daemon, Docker system process, or primary database rather than the rogue container.
Proper resource allocation prevents noisy neighbor problems in multi-tenant environments. For example, if you are handling Telegram bot rate limits with Redis, you want to guarantee that your Redis cache container always has allocated RAM while ensuring that background scraping or logging containers cannot starve the core database. Docker uses Linux Control Groups (cgroups) under the hood to enforce these boundaries cleanly at the kernel level.
Restricting CPU Allocations
Docker controls CPU usage using the Linux kernel Completely Fair Scheduler (CFS) and cgroups. You can limit CPU consumption through three primary methods: hard limits on CPU counts, core pinning, and relative CPU shares.
Setting Hard CPU Limits
The --cpus flag specifies the maximum number of CPU cores a container can use during any given scheduling period. This value can be a fraction or an integer.
For instance, to limit a container to a maximum of 1.5 CPU cores, execute the following command:
docker run -d --name restricted-app --cpus="1.5" nginx:alpine
Under cgroups v2, this translates into setting CFS quota and period parameters. The default period is 100 milliseconds (100000 microseconds). A setting of --cpus="1.5" tells the kernel scheduler that the container can consume at most 150000 microseconds of CPU time every 100000 microseconds.
Pinning Containers to Specific CPU Cores
If your server has multiple physical cores and you want to dedicate specific cores to high-priority workloads, use the --cpuset-cpus flag. This eliminates CPU context switching overhead across different physical sockets or cores.
To pin a container strictly to the first and third CPU cores (core indices 0 and 2):
docker run -d --name pinned-worker --cpuset-cpus="0,2" python:3.11-slim python3 -c "while True: pass"
You can also specify ranges of CPU cores:
docker run -d --name core-range-app --cpuset-cpus="0-3" node:20-alpine node index.js
Configuring Relative CPU Shares
The --cpu-shares flag sets a relative weight for CPU distribution when multiple containers compete for CPU cycles. The default weight is 1024.
This flag does not impose a hard ceiling. If only one container is active, it can use 100 percent of the CPU. However, when multiple containers demand CPU resources simultaneously, the kernel distributes processor time according to their relative weights.
To run two competing containers where container A gets twice as much CPU time as container B:
docker run -d --name container-high-priority --cpu-shares=2048 ubuntu stress --cpu 4
docker run -d --name container-low-priority --cpu-shares=1024 ubuntu stress --cpu 4
In this scenario, container-high-priority receives 66.6 percent of the CPU cycles, while container-low-priority receives 33.3 percent during periods of high resource contention.
Restricting Memory and Swap Allocation
Memory management in Docker involves configuring hard memory limits, soft memory reservations, swap limits, and adjusting the kernel OOM killer behavior.
When building reliable structured output pipelines with OpenAI, background Python scripts or parsing workers can consume variable amounts of memory depending on payload size. Setting strict limits prevents a single large API response from exhausting your server host memory.
Hard Memory Limits (--memory)
The --memory (or -m) flag sets the maximum amount of RAM a container can consume. If the container attempts to allocate more memory than this threshold, the kernel triggers an internal OOM condition within the container’s cgroup.
To restrict a container to a strict maximum of 512 Megabytes of RAM:
docker run -d --name limited-mem-app --memory="512m" redis:7-alpine
If the container process exceeds 512 MB, Docker’s default behavior is to kill the process inside the container with an OOM error (exit status 137).
Swap Space Limits (--memory-swap)
Swap space allows the host system to write excess memory pages to disk when RAM is full. However, excessive swapping degrades system performance.
The --memory-swap flag controls the combined total of RAM and Swap available to a container. The behavior depends on how --memory and --memory-swap are configured together:
- If
--memoryis set to512mand--memory-swapis set to1g, the container can use 512 MB of RAM and 512 MB of Swap space (1 GB total minus 512 MB RAM). - If
--memoryis set to512mand--memory-swapis set to512m, swap space is entirely disabled for this container. - If
--memoryis set to512mand--memory-swapis unset, the container can access default system swap equal to twice the RAM limit if swap is enabled on the host.
To run a process with 1 GB of RAM and disable swap entirely:
docker run -d --name no-swap-app --memory="1g" --memory-swap="1g" postgres:16-alpine
Soft Memory Limits (--memory-reservation)
The --memory-reservation flag sets a soft limit lower than --memory. Under normal system operations, the container can freely consume memory up to its hard limit. However, when host memory is constrained, the kernel active reclaim mechanism forces the container to reduce its memory usage down to the soft limit reservation level.
docker run -d --name soft-limit-app --memory="1g" --memory-reservation="256m" node:20-alpine
💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.
Controlling Disk I/O Limits
By default, a container can read and write to host storage devices as fast as the underlying disk hardware allows. If an active container writes massive volume logs or conducts heavy disk operations, it can saturate the disk controller queue, stalling other applications on the server.
When you are handling webhook retries using exponential backoff strategies, high transaction rates or burst failures can generate elevated log volume. Regulating disk throughput ensures disk write saturation does not lock up your host OS.
Docker allows you to control Block I/O using device read/write throughput rates (Bytes Per Second) and operational frequencies (IOPS).
Limiting Read and Write Rates (BPS)
To limit disk write speed to 10 Megabytes per second and read speed to 20 Megabytes per second for a specific storage device (such as /dev/sda), pass the --device-write-bps and --device-read-bps flags along with the block device path:
docker run -d --name io-restricted-app --device-write-bps /dev/sda:10mb --device-read-bps /dev/sda:20mb ubuntu tail -f /dev/null
To test these limits inside the running container using dd:
docker exec -it io-restricted-app dd if=/dev/zero of=/test.img bs=1M count=100 oflag=direct
The output will confirm that the disk write speed is capped precisely at 10.0 MB/s.
Limiting Operations Per Second (IOPS)
For fast NVMe drives where total input/output operations per second cause system bottlenecks, use --device-read-iops and --device-write-iops:
docker run -d --name iops-restricted-app --device-write-iops /dev/sda:500 alpine tail -f /dev/null
This configuration caps disk write commands to 500 operations per second regardless of individual payload sizes.
Implementing Resource Limits in Docker Compose
In production, infrastructure configuration should be version-controlled rather than executed via raw command line strings. Docker Compose V2 supports resource constraints using the deploy.resources specification block.
Below is a production-ready docker-compose.yml file defining three services with CPU, memory, swap, and disk resource restrictions:
version: '3.8'
services:
web-app:
image: nginx:1.25-alpine
container_name: production-web
ports:
- "80:80"
restart: always
deploy:
resources:
limits:
cpus: '0.75'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
redis-cache:
image: redis:7.2-alpine
container_name: production-redis
command: redis-server --appendonly yes
ports:
- "6379:6379"
restart: always
deploy:
resources:
limits:
cpus: '0.50'
memory: 256M
reservations:
cpus: '0.10'
memory: 64M
background-worker:
image: node:20-alpine
container_name: production-worker
command: node index.js
restart: always
mem_swappiness: 0
deploy:
resources:
limits:
cpus: '1.00'
memory: 1024M
reservations:
cpus: '0.50'
memory: 256M
blkio_config:
weight: 300
device_read_bps:
- path: /dev/sda
rate: 20mb
device_write_bps:
- path: /dev/sda
rate: 10mb
To run this stack with enforced resources:
docker compose up -d
Docker Compose validates this structure and instructs the underlying container execution engine to construct cgroups with the specified memory boundaries, swap behavior, and scheduler constraints.
Monitoring Container Resource Usage
Setting limits is only half the process; you must continuously monitor containers to verify that resource thresholds are defined correctly and processes are not continuously reaching OOM thresholds.
Live CLI Monitoring
The docker stats command streams live CPU, RAM, Network I/O, and Block I/O statistics for all active containers on the host machine:
docker stats
To receive a clean single snapshot in JSON or tabular format without continuous streaming:
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.BlockIO}}"
An example execution output looks like this:
NAME CPU % MEM USAGE / LIMIT MEM % BLOCK I/O
production-web 0.04% 12.4MiB / 512MiB 2.42% 1.2MB / 4.1MB
production-redis 0.12% 8.85MiB / 256MiB 3.46% 512kB / 12MB
production-worker 45.20% 312MiB / 1.024GiB 30.47% 8.4MB / 1.2MB
Inspecting Container Cgroups Directly
You can directly inspect the Linux kernel cgroup files to verify applied constraints on system performance. On systems using cgroups v2, Docker writes memory limits to /sys/fs/cgroup/.
To read the exact byte-level memory limit for a running container:
CONTAINER_ID=$(docker inspect --format='{{.Id}}' production-web)
cat /sys/fs/cgroup/docker/$CONTAINER_ID/memory.max
If the command returns 536870912, the 512 MB memory boundary is verified at the kernel layer (512 x 1024 x 1024 bytes).
If a process inside a container gets terminated unexpectedly, inspect the container state to check if the OOM Killer was triggered:
docker inspect production-web --format='{{.State.OOMKilled}}'
If this returns true, the process exceeded its allocated memory limits and was stopped by the kernel. You must either optimize your software code to handle memory more efficiently or adjust your --memory limit higher in your deployment files.
Getting Started
To implement these container limits across your server fleet:
- Provision a high-performance instance on a Hetzner VPS or a DigitalOcean droplet.
- Deploy your application stack using the provided
docker-compose.ymlresource limit template. - Map your production domain via Namecheap to point traffic to your public web containers.
- Execute
docker statsto verify that CPU and RAM usage stay comfortably within configured boundaries.
Outsource Your Automation
Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.
Want to automate this yourself?
Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.
Want this engine running on your own VPS?
This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.
Get System 3