By default, a Docker container can eat as much RAM as the Linux kernel lets it. There’s no ceiling unless you set one. That’s fine on a 64GB dedicated box. It’s a real problem on the $5-6/month, 1GB VPS most indie hackers actually deploy on, where a single memory leak can take down your app, your database, and your reverse proxy in one shot. Getting docker container memory limits right means picking a number that won’t crash the box or waste half of it. To show what the failure actually looks like, we forced an OOM kill on a fresh $5/month, 1GB Vultr instance; the exact commands and kernel output are in our test section below.

Why Docker containers need a memory limit
Docker’s own documentation is blunt about this: test your application to understand its real memory requirements, run it only on hosts with adequate resources, and limit how much memory any single container can consume. Skip that last step and you’re one bad deploy away from an outage that has nothing to do with your code, just a container that grew without a ceiling.
This matters more on small infrastructure than on large. A 64GB server can absorb a leaking container for hours before anyone notices. A 1GB VPS running an app, a database, and a proxy has no slack at all, so one runaway process degrades or kills everything else sharing the box.
How Docker enforces memory limits
Docker hands memory enforcement to Linux cgroups. When you run docker run -m 512m nginx, Docker writes that value into the container’s cgroup memory controller (memory.max on cgroup v2 hosts, memory.limit_in_bytes on cgroup v1). Cross that ceiling and the kernel’s out-of-memory killer picks a process inside that cgroup and kills it with no warning and no cleanup.
You can read the live numbers directly instead of trusting docker stats alone: docker exec mycontainer cat /sys/fs/cgroup/memory.current shows current usage, and cat /sys/fs/cgroup/memory.max shows the configured ceiling. docker stats reports memory after subtracting page cache, so the number you see is closer to real application memory than the cgroup’s raw accounting, which is why docker stats can look fine right up until the kernel kills something.
How to limit Docker memory: flags and syntax
The core flag is -m or --memory, and it accepts a number plus a suffix: b, k, m, or g. The minimum Docker allows is 6m; anything smaller throws an error.
# Limit a container to 512MB
docker run -m 512m nginx
# Limit a container to 2GB
docker run --memory 2g my-app
Three related flags shape how that limit behaves:
--memory-swapsets the combined RAM + swap ceiling, not swap alone.--memory=300m --memory-swap=1ggives the container 300MB of RAM plus 700MB of swap. Setting--memory-swapequal to--memorydisables swap entirely, the common choice when you’d rather fail fast than thrash to disk:
# No swap — hit the OOM killer instead of slowing to a crawl
docker run --memory=512m --memory-swap=512m my-webapp
--memory-reservationis a soft floor below the hard cap. Docker only tries to push usage back toward it when the host is under pressure; the container can burst above it freely otherwise. This mirrors the “reservation vs. limit” pattern Kubernetes and Compose both use.--oom-kill-disablestops the kernel from killing the container on OOM. Docker’s guidance is explicit: only use this alongside--memory. Without a cap, a container with OOM-kill disabled can consume all host memory and take everything else down with it.
Memory-limit flag builder
Pick a limit and a swap policy; get the matching docker run flags and Compose keys.
Must be below the hard limit. Leave empty for none.
Docker memory limit example: docker-compose.yml
Most indie hacker stacks run an app, a database, and a proxy together, which is exactly where docker run flags stop being practical. Compose handles this through the deploy.resources block:
services:
app:
image: my-app:latest
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '1.0'
memory: 1G
postgres:
image: postgres:16
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
nginx:
image: nginx:alpine
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
limits.memory is the hard cap, reservations.memory is the soft floor Docker tries to guarantee. One caveat worth knowing: the deploy key’s resource limits were historically Swarm-only in older Compose releases. Current Compose v2 generally honors them on a plain docker compose up, but if your limits don’t seem to apply, confirm your installed Compose version actually supports this outside Swarm mode before assuming your YAML is wrong. Check live usage per service with docker compose stats app.

We forced an OOM kill on a $5, 1GB VPS
Everything above is Docker’s documented behavior, so we reproduced the failure ourselves to show what it looks like on the kind of box this post is about. On 2026-09-25 we created a fresh Vultr Cloud Compute (Regular) instance in Tokyo (plan vc2-1c-1gb: 1 vCPU, 1GB RAM, $5/month) running Ubuntu 24.04, with Docker 29.8.1 installed from get.docker.com. Each command ran once. This is a reproduction of the mechanism, not a long-term benchmark.
The test is deliberately blunt: a python:3.12-slim container capped at 128MB of RAM, with --memory-swap set to the same 128MB so it gets no swap, tries to allocate a 300MB byte array.
docker run --name oomtest --memory=128m --memory-swap=128m python:3.12-slim python3 -c "b = bytearray(300*1024*1024); print(\"allocated\")"
echo "exit code: $?"
docker inspect oomtest --format "OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}"
dmesg | grep -iE "memory cgroup out of memory|oom-kill|Killed process" | tail -3
Output, trimmed to the relevant lines (the long cgroup path in the second kernel line is shortened with “…”):
exit code: 137
OOMKilled=true ExitCode=137
[ 658.419684] python3 invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=0
[ 658.420013] oom-kill:constraint=CONSTRAINT_MEMCG,…,task=python3,pid=4267,uid=0
[ 658.420030] Memory cgroup out of memory: Killed process 4267 (python3) total-vm:321376kB, anon-rss:130432kB, file-rss:5504kB, shmem-rss:0kB, UID:0 pgtables:328kB oom_score_adj:0
print("allocated") never ran. The process died with exit code 137, docker inspect recorded OOMKilled=true, and the kernel log names the victim. Two fields are worth reading closely. constraint=CONSTRAINT_MEMCG says the kill was triggered by the container’s own memory cgroup (the limit we set) rather than the host as a whole running out. And anon-rss:130432kB is about 127MB: the process was killed right at its 128MB ceiling, long before it got anywhere near 300MB, even though its virtual size (total-vm:321376kB) already covered the full array.
Then the same image and the same 300MB allocation, with the only change being a 512MB limit:
docker run --rm --memory=512m --memory-swap=512m python:3.12-slim python3 -c "b = bytearray(300*1024*1024); print(\"allocated 300MB fine\")"; echo "exit code: $?"
allocated 300MB fine
exit code: 0
That’s the whole failure in miniature: a workload that needs more memory than its cgroup allows gets killed, and the same workload under a higher limit finishes. In production you’ll recognize it by the same three signals: exit code 137, OOMKilled=true in docker inspect, and a Memory cgroup out of memory: Killed process line in dmesg. Real apps are rarely this clean, though, so before you simply raise the number, work through the diagnosis checklist below to rule out leaks and shutdown bugs.
Why we set --memory-swap explicitly. Right after installing Docker, free -m on this 1GB instance showed that Vultr’s Ubuntu image ships with swap already enabled:
total used free shared buff/cache available
Mem: 955 354 177 1 576 601
Swap: 2399 0 2399
That matters for the flags above. Docker’s documentation says that when you set --memory but leave --memory-swap unset, the container can use as much swap as its memory limit (twice the --memory value in total) if the host has swap configured. On a host like this one, a bare --memory=128m would therefore allow up to 256MB of RAM plus swap, not a strict 128MB. We didn’t test that unset case; we pinned --memory-swap=128m so the run measured the RAM limit alone. If you want the fail-fast behavior shown here, set both flags.
Diagnosing an OOM kill (exit code 137)
Exit code 137 is 128 + 9, or SIGKILL. It means the container’s main process was killed abruptly, with no chance to clean up. The usual cause is the OOM killer, triggered by either the container’s own --memory limit or the host running out of RAM generally.
Work through it in order:
- Check if Docker itself recorded the kill:
docker inspect <container_id> --format='{{.State.OOMKilled}}'.trueconfirms it exceeded its memory limit. - If
OOMKilledisfalsebut the exit code is still 137, checkdmesg: the host itself may have run out of memory. - If 137 happens during
docker stop, the cause usually isn’t memory at all. The app isn’t handlingSIGTERMfast enough within Docker’s stop timeout, and Docker escalates toSIGKILL. That’s a shutdown-handling bug, not a sizing bug. - For suspected leaks, watch
docker statsor the cgroup files over time instead of reacting to one kill event.
A mistake that shows up across nearly every source on this topic: treating “just raise the limit” as the universal fix. It’s the right move only when the container is genuinely under-provisioned. If the real cause is a memory leak, an unbounded cache, or a shutdown bug, raising the limit just delays the next crash.
Why Node.js and Java containers get killed even when they “fit”
Setting a cgroup limit doesn’t tell a language runtime how much memory it’s allowed to use, and this mismatch is the single most common cause of confusing OOM kills in application containers.
Node.js: older versions size the V8 heap based on total host memory, not the container’s cgroup limit, so a container can get OOM-killed while using only 50-60% of its stated cap because V8 never felt enough pressure to garbage-collect. Node.js 20+ made heap sizing container-aware by default, but plenty of production images still run older LTS releases or set NODE_OPTIONS=--max-old-space-size incorrectly. A workable rule of thumb: allocate 60-75% of the container’s memory limit to the V8 heap, leaving the rest for buffers and native memory. A 512MB container might use --max-old-space-size=350, keeping roughly 160MB in reserve. Think of it as container limit = heap + buffers/native memory + safety margin, not container limit = heap.
Java: the fix is -XX:+UseContainerSupport (on by default in modern JDKs) paired with -XX:MaxRAMPercentage, which sizes the heap as a percentage of the detected container limit instead of a fixed -Xmx value that may badly over- or under-shoot the actual cgroup ceiling.
The underlying principle applies to any runtime that manages its own heap independently of the OS: the cgroup limit constrains total resident set size (heap plus everything else), so a runtime that doesn’t know about that ceiling will guess wrong, and it usually guesses too high.

Choosing the right Docker container memory limit for your VPS
Nearly every guide on this topic stops here. The standard advice, “test under realistic load, watch docker stats, set the limit slightly above observed peak”, is technically correct and answers nothing useful if you’re staring at a fresh $6/month VPS and three containers you haven’t deployed yet.
Sizing is situational: it depends on your VPS’s total RAM, how many containers are sharing it, and what each one actually does. Rather than manually watching docker stats for days and doing napkin math across every service on the box, you can plug your VPS size and container count into our Docker RAM Calculator and get a starting allocation to test from. It turns “I have 1GB and three containers” into actual --memory values for your app, database, and proxy.
If you’re consistently hitting memory pressure even after right-sizing each container, the fix is more headroom on the box itself. For context, here’s what that costs as of mid-2026:
| Provider | Plan | RAM | Price |
|---|---|---|---|
| Vultr Cloud Compute (Regular) | Entry | 1GB | $5/month |
| Vultr Cloud Compute (High Performance) | Entry NVMe | 1GB | $6/month |
| Hetzner CX23 | 2 vCPU, 40GB NVMe, 20TB traffic | 4GB | ~€5.49/month |
Prices last checked: 2026-07-16.
For most Docker-on-a-budget setups, jumping from a 1GB box to Hetzner’s 4GB CX23 buys you real headroom for less than doubling your bill. It’s worth it the moment you’re regularly seeing exit code 137 across multiple containers rather than just one misbehaving service. If you’d rather stay in the Vultr ecosystem and just bump plan size, Vultr offers new customers up to $300 in promo-code credit, valid for up to 30 days on select products, enough runway to test a larger instance without committing to it.
We’ve also covered the deployment side directly: our full Docker on Vultr setup guide walks through getting the daemon running before you start tuning memory limits, and our Vultr $6 vs Hetzner CX23 comparison breaks down that entry-tier pricing decision in more depth.
Related reading: deploying Docker on a Vultr VPS, sizing RAM for a Minecraft server.
Where to start
Set a hard --memory limit on every container you run in production. Leaving it unset means there is no limit at all. Start with --memory-swap equal to --memory so you fail fast instead of thrashing to disk, add --memory-reservation if you’re running multiple containers that need to share headroom fairly, and check your runtime’s heap flags (--max-old-space-size, -XX:MaxRAMPercentage) before assuming a cgroup limit alone will protect you. Our one-off test on a $5, 1GB Vultr instance shows why the swap flag belongs in that list: with --memory=128m --memory-swap=128m, a 300MB allocation was killed cleanly with exit code 137 and OOMKilled=true, scoped to that one container’s cgroup, while a 512MB limit let the same allocation finish. That loud, contained failure is what you want your limits to produce. If you’re not sure what numbers to start with, run your VPS size and container list through the Docker RAM Calculator rather than guessing. It’s faster than watching docker stats for a week and safer than finding out the hard way at 2 a.m.

