For a long time, I treated Docker resource limits as the kind of thing you only configure after a host has already taught you a painful lesson.
The defaults felt generous.
Most services looked small.
And “I will come back and tune it later” sounded responsible enough in the moment.
That story works right up until one container stops being polite.
Docker’s own documentation begins the subject with the most important sentence: by default, a container has no resource constraints and can use as much of a given resource as the host’s kernel scheduler allows.
That is the whole reason this article matters.
Because an unbounded container does not need to be malicious to become expensive. It only needs one noisy process, one memory leak, one unexpectedly heavy import, or one bad day to turn the rest of the machine into collateral damage.
- Why resource limits matter more than they first appear to
- Why RAM limits are really about host protection
- What soft memory limits do and do not promise
- How swap changes the memory story
- What CPU limits actually mean in practice
- Why soft CPU weighting is not the same as a hard cap
- The limit strategy I now trust for small homelabs
For the technical baseline below, I use Docker's official documentation and separate documented platform behavior from my own placement and operating recommendations.
Resource limits matter because containers are not automatically considerate
This was the first mental correction that helped me.
It is easy to assume a small service will stay small forever. Sometimes that is true. But Docker does not begin from the assumption that containers should be quiet roommates. It begins from the assumption that the host scheduler is in charge unless you explicitly say otherwise.
That means limits are not mainly about squeezing every percentage point of efficiency from the machine. They are about setting boundaries before one workload gets a chance to make boundary decisions for everyone else.
This is very similar to the lesson behind container log management. The host should not rely on luck and politeness. It should rely on policy.
Memory limits are the first limits I care about
If I could set only one class of limit for a risky or poorly understood container, I would start with memory.
Docker’s resource-constraints documentation is very clear that it is important not to let a running container consume too much of the host machine’s memory. On Linux hosts, if the kernel detects insufficient memory to perform important system functions, it throws an out-of-memory exception, and any process can be subject to killing, including Docker and other important applications.
That is the real reason memory limits matter.
They are not only performance settings. They are blast-radius settings.
The simplest hard memory boundary is:
docker run -d \
--memory="512m" \
my-image
Or in Compose:
services:
app:
image: my-image
mem_limit: 512m
What I like about memory limits is that they force me to answer a healthy question:
How much RAM am I actually willing to let this service consume before I would rather it fail than hurt the rest of the host?
That is a much better question than “how much RAM can I spare emotionally?”
Soft memory limits are guidance, not protection
This is one of the easiest places to misread the UI or the flag names.
Docker supports --memory-reservation, and the docs describe it as a soft limit smaller than --memory that is activated when Docker detects contention or low memory on the host machine. The docs also say something very important: because it is a soft limit, it does not guarantee that the container does not exceed the limit.
That sentence is the key.
So if I use:
docker run -d \
--memory="1g" \
--memory-reservation="512m" \
my-image
I am not saying:
“Stay under 512 MB.”
I am saying something more like:
“Try to behave gently around 512 MB, but the real wall is 1 GB.”
That is useful, but it is not the same thing as a hard guardrail.
This is why I do not like calling soft limits “safe” in conversation. They are helpful scheduling hints under pressure. They are not host-protection guarantees.
Swap is where the memory story gets more subtle
This part confused me longer than I expected.
Docker treats --memory-swap as a modifier that only has meaning if --memory is also set. The docs explain several cases, but the practical one I remember is this:
- if
--memory-swapequals--memory, the container is prevented from using swap; - if
--memory-swapis left unset while--memoryis set, the container can use swap up to roughly the same amount as the memory limit again; - if the host does not support swap limits, Docker may warn you with
WARNING: No swap limit support.
That means “512 MB memory limit” is not always emotionally equivalent to “this thing can only pressure the machine with 512 MB total behavior,” because swap policy changes what happens after RAM pressure starts.
I do not think every small homelab needs an elaborate swap philosophy. But I do think the operator should know whether a limit is really a hard edge or whether the container can spill its pain onto disk afterward.
CPU limits are about fairness and scheduling shape more than hard possession
This is where I think people often carry the wrong instinct from VMs into containers.
Docker gives you several CPU-related controls:
--cpus--cpu-shares--cpuset-cpus
The easiest hard-ish mental model is --cpus.
Docker documents --cpus=<value> as a way to specify how much of the available CPU resources a container can use. For example, --cpus="1.5" means the container is guaranteed at most one and a half CPUs worth of time.
That makes this a very practical control:
docker run -d \
--cpus="1.5" \
my-image
I like --cpus because it sounds like what it means.
It does not ask me to think in scheduler internals first. It just lets me say: this container may use up to this much CPU capacity.
That is often enough for small homelab reasoning.
CPU shares are soft weighting, not a real ceiling
This is the second place people confuse “preference” with “limit.”
Docker’s docs say --cpu-shares changes a container’s weight relative to the default of 1024, giving it access to a greater or lesser proportion of host CPU cycles. But the docs also state two critical things:
- this is only enforced when CPU cycles are constrained;
- it does not guarantee or reserve any specific CPU access.
That means --cpu-shares is a fairness hint under contention.
It is not a promise that the container is capped in the way --cpus feels capped.
This is why I think --cpu-shares is useful when you already understand the host’s behavior and want relative priority tuning. It is not the first thing I reach for when my actual goal is “do not let this container run away with the box.”
CPU pinning is precise, but it is not the same as general limit-setting
Docker also offers --cpuset-cpus, which limits a container to specific CPUs or cores.
That is a different kind of control:
docker run -d \
--cpuset-cpus="0,1" \
my-image
This is less about broad fairness and more about placement.
I think of it the same way I think about topology or pinning conversations in VMs, like in the CPU allocation article. It can be useful when you know why you care about placement. It is not the first limit I recommend to someone who just wants to stop one container from being greedy.
In most ordinary homelabs, I would rather start with:
- a believable memory cap;
- a believable CPU cap via
--cpus; - then observe real behavior.
Observe first, then tighten intelligently
This is where docker stats becomes useful.
Docker’s docker stats command provides a live stream of container resource usage statistics. I like it because it keeps the feedback loop close to the actual behavior rather than to the configuration fantasy.
A simple workflow looks like this:
- start with conservative but believable limits;
- run the service under normal use;
- watch
docker stats; - see whether the service is genuinely constrained or just comfortably bounded;
- adjust only when there is evidence.
That process feels much healthier than either of the two common extremes:
- never set limits at all;
- set dramatic limits immediately and assume you just became more disciplined.
Boundaries are supposed to make the host safer, not make the service mysteriously fragile.
The limit strategy I trust now
This is the rule I would give my earlier self.
If a container is important, noisy, or not yet fully understood:
- set a hard memory limit;
- consider a softer reservation if you want earlier pressure behavior;
- understand what swap is allowed to do;
- use
--cpusif you want a practical CPU ceiling; - treat
--cpu-sharesas fairness tuning, not as a hard cap; - use
docker statsto verify behavior instead of guessing.
That approach feels especially right in a small homelab, where one host may be carrying many unrelated jobs and where the wrong kind of generosity can make the whole machine feel unstable.
This is also why container limits and VM sizing belong in the same family of thinking as memory ballooning. The central question is always the same: am I describing a boundary honestly, or am I relying on optimism because the machine has not complained yet?
This operating decision also connects to Portainer vs Yacht vs Dockge: Choosing a Web UI for Docker Containers and Podman vs Docker for Homelabs: Daemonless Containers Explained, where the same tradeoff appears at a different layer of the homelab.
FAQ
Do Docker containers have CPU and RAM limits by default?
No. Docker’s documentation is explicit that by default a container has no resource constraints and can use as much of a resource as the host scheduler allows.
Which limit matters most first in a homelab?
Usually memory. An unbounded memory-hungry container can pressure the entire host and trigger out-of-memory behavior that affects far more than just the container itself.
Is --memory-reservation enough by itself?
Not if you want a real hard boundary. Docker describes it as a soft limit that does not guarantee the container stays under that amount. It is useful guidance, not a strict wall.
Is --cpu-shares the same as giving a container a CPU cap?
No. Docker documents CPU shares as a relative weight enforced when CPU is constrained. It helps with fairness but does not guarantee or reserve a specific amount of CPU access.
What is the easiest CPU limit to understand?
Usually --cpus, because it directly expresses how much total CPU capacity a container may use, such as --cpus="1.5".



