Tutorial

How to Limit Docker Container CPU and Memory the Right Way (cgroup v2)

Back to Blog
Managing servers the hard way? Panelica gives you isolated hosting, built-in Docker and AI-assisted management.
Start free

By default, a Docker container can use every CPU core and every byte of memory on your server. One misbehaving app — a runaway import job, a memory leak, a crypto-miner in a compromised image — can starve everything else you host. Limiting container resources is how you turn "one bad container took the server down" into "one bad container got throttled." This guide explains how CPU and memory limits actually behave under cgroup v2, how to choose sane values, and how Panelica applies them per container and per hosting account.

What happens when a container hits its limit?

The two limits behave very differently, and knowing the difference saves you from misreading incidents:

LimitWhen exceededSymptom you see
CPU (--cpus)The container is throttled — the kernel simply gives it fewer time slicesApp gets slower; no crash, no error
Memory (--memory)The kernel's OOM killer terminates a process inside the containerApp crashes or restarts; exit code 137

This is why the classic mystery — "my container keeps restarting with exit code 137" — is almost always a memory limit, not a bug in the app. And why "the app got slow after we added limits" is CPU throttling doing exactly what you asked.

How cgroup v2 enforces this

On a modern Linux distribution, Docker delegates enforcement to cgroup v2, the kernel's resource controller. Every container becomes a cgroup with files like:

cpu.max       "150000 100000"   # 1.5 cores: 150ms of CPU time per 100ms period
memory.max    536870912          # hard cap: 512 MB
pids.max      512                # optional: cap process count (fork bombs)

The useful mental model: cpu.max is a budget per time period (exceed it and you wait for the next period — that is the throttle), while memory.max is a hard wall (exceed it and the OOM killer picks a victim inside your cgroup).

Cgroups are hierarchical, and that hierarchy is the interesting part. A container's cgroup can be placed under a parent group, and the parent's limits cap the total of everything inside it. Docker exposes this as --cgroup-parent.

Why the parent group matters for hosting

Per-container limits answer "how much can this app use?" The parent group answers a more important question on a shared server: "how much can this user use, in total?"

Panelica uses exactly this mechanism. Every hosting account on a Panelica server has its own cgroup slice with CPU, memory, I/O, and process limits. When a user deploys a container, the panel sets the container's cgroup parent to that user's slice. The result:

  • The container's own limits (set in the deploy form) cap the individual app.
  • The account's slice caps the sum of that user's containers, PHP processes, and cron jobs together.
  • Ten containers from one account cannot collectively exceed the account's allocation — the kernel enforces it, not a monitoring script that reacts after the fact.

If you want the deeper story on how slice-based isolation compares to older approaches, see our cgroup v2 isolation write-up.

Choosing values that will not bite you

There is no universal number, but there is a reliable method:

  1. Start from the app's known baseline. Panelica's app templates ship a minimum memory value per app (a small utility might need 128 MB; a browser-based desktop needs 2 GB). Treat the minimum as a floor, not a target.
  2. Watch the real usage for a week. The panel streams live CPU and memory stats per container. Note the normal peak, not the average.
  3. Set memory to peak plus headroom — 50 to 100 percent above observed peak is a reasonable buffer. Too tight, and routine spikes (an update, a large upload, a reindex) trigger OOM kills that look like random crashes.
  4. Set CPU by fairness, not by fear. CPU throttling degrades gracefully, so you can be stricter. On a 4-core server hosting six apps, giving each 1 to 1.5 cores works fine — they rarely peak together.
  5. Do not forget shared memory for browser-based apps. Anything running Chromium inside (web desktops, PDF renderers, screenshot services) needs an increased --shm-size, or tabs crash with cryptic errors long before the memory limit is reached. Panelica exposes shm-size in the container's advanced settings.

Setting limits in Panelica vs the CLI

The docker run equivalent looks like this:

docker run -d --name myapp \
  --cpus="1.5" \
  --memory="512m" \
  --shm-size="256m" \
  --cgroup-parent="panelica-user-alice.slice" \
  myimage:latest

In Panelica, the same values are fields on the container create form and in the app template deploy dialog — CPU cores, memory, shared memory — and the cgroup parent is applied automatically based on which account is deploying. Limits are also copied when you clone a stack for staging, so a test copy inherits the same guardrails as production.

Frequently asked questions

Should I limit every container?

Memory: yes, always — an unlimited container plus a memory leak equals a frozen server. CPU: strongly recommended on shared servers; optional on a single-app server where the app is supposed to use everything.

Why did my container get killed below its memory limit?

Two usual causes: the account-level parent group hit its total limit (the sum of all your containers), or the host itself ran out of memory and the kernel OOM killer chose a victim globally. Check the account's overall usage, not just the container's.

Do limits cost performance when the container stays under them?

No. Cgroup accounting is part of the kernel's normal scheduling path; a container running below its limits behaves identically to an unlimited one.

Is a swap limit needed too?

On servers, the common practice is to keep containers out of swap entirely — swapping app memory turns a memory problem into a much harder-to-diagnose latency problem. Set memory limits honestly instead.

The takeaway

CPU limits throttle, memory limits kill — design around that asymmetry. Give every container a memory cap with real headroom, give shared servers per-account parent groups so totals are enforced by the kernel, and watch actual usage before tightening anything. Panelica bakes all three layers into the Docker Manager: per-container limits on the deploy form, per-account cgroup slices applied automatically, and live stats to tell you what the numbers should be.

Security-first hosting panel

Stop bolting tools onto a legacy panel.

Panelica is a modern, security-first hosting panel — isolated services, built-in Docker and AI-assisted management, with one-click migration from any panel.

Zero-downtime migration Fully isolated services Cancel anytime
Share:
Looking for a Plesk alternative?