Docker and VPS production hardening checklist: caps, OOM proof, disk limits

By Sudipta Dhara / October 23, 2026

By Sudipta Dhara

Published 2026-10-10

Last updated 2026-10-18

AI disclosure: drafted with AI assistance.

Disclosure: this page has no affiliate or referral links.

Who this is for: developers and DevOps engineers · Intermediate · You’ll need: root on a Linux host running Docker Engine, Compose optional, and about 30 minutes

Your VPS feels fine until one container eats the RAM, fills /, or dies with exit 137 and you have no proof of which limit fired. docker stats is green one minute and the host is swapping the next.

Direct answer: Put a memory cap and a matching swap cap on every container that shares the host, prove one real job under that cap, then cap disk with log rotation and a measured prune — before you buy more RAM.

Doesn’t apply when: you run Kubernetes LimitRanges, a managed serverless product, or a single dedicated host that already has one process and no neighbours.

How we test (review criteria / methodology)

This checklist only cites numbers we already measured on published ProDevs guides. Memory OOM pairs come from the 29 September 2026 docker run lab on Docker Engine 28.5.1 (Docker memory limits). Idle Postgres floors come from the 29 September 2026 postgres:16-alpine run (Idle Postgres RAM). Disk log growth and prune behaviour come from the 5–9 October 2026 test VM on Docker CE 29.8.2 (Docker disk full). Kernel-vs-VM context comes from the Linux VPS reading on Docker Engine 28.5.1 (Docker or a VPS). We do not invent new lab numbers on this page.

Primary sources checked next to a date (CR-03): on 29 September 2026 we re-read Docker docs: resource constraints for equal --memory / --memory-swap behaviour. On 29 September 2026 we re-read PostgreSQL docs: resource usage for the shared_buffers starting advice. On 18 October 2026 we re-read kernel Documentation: /proc filesystem for the OOM-killer fields that show up when a cgroup kill hits. On 5 October 2026 we re-read Docker docs: configure logging drivers for the default of no log rotation.

Who it’s for

You run Docker on one VPS or a small homelab host, you share that host across a few containers, and you want a single checklist that ties memory caps, a real OOM check, and disk control together.

Not for

Kubernetes pod limits, JVM or Node heap tuning, cloud auto-scaling policy design, and “which VPS vendor is cheapest” shortlists are other jobs. This page stays on one Linux Docker host.

What broke when we skipped the checklist

We kept uncapped neighbours next to work we cared about. When a job climbed, the kernel killed a process and the host looked “too small.” Buying RAM first would have hidden the neighbour. The fix that stuck was equal --memory and --memory-swap, a job run that printed OOMKilled and exit 137, then json-file log limits that only applied after re-create — the same traps the linked labs document. Disk filled later for a different reason: an old container kept writing unbounded logs after daemon.json changed.

How do I set memory caps?

  1. Read steady use while the app does ordinary work:
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}'
  1. Set --memory and --memory-swap to the same value, about 1.5× that steady line, or above the heavy job if the job is higher (Docker memory limits; Docker docs: resource constraints checked 29 September 2026).
  2. In Compose, copy the same pair as mem_limit / memswap_limit or deploy.resources.limits.memory with swap disabled the same way.
  3. Never leave a production neighbour uncapped on a shared host. By default a container can use as much memory as the host scheduler allows (Docker docs: resource constraints checked 29 September 2026).

Shareable fact: equal --memory and --memory-swap turn swap off for that container; leave --memory-swap unset and Docker may allow about twice the memory value as memory+swap when the host has swap (Docker docs: resource constraints checked 29 September 2026). Caveat: host swap support must be on (SwapLimit=true in docker info).

How do I prove the OOM instead of guessing?

  1. Run one real job (import, test suite, or a known allocate) under the cap.
  2. On exit, inspect:
docker inspect -f '{{.State.OOMKilled}} {{.State.ExitCode}}' NAME
  1. Treat OOMKilled=true and exit 137 as “the cap worked; raise both numbers or hold less,” not as “the VPS is broken.” The host still exposes OOM fields under /proc when you need the kernel side of the kill (kernel Documentation: /proc filesystem checked 18 October 2026).
  2. Only-we-have lab (29 September 2026, Docker Engine 28.5.1, linux/amd64 host, MemTotal ≈ 47.05GiB): an 8 MiB payload under a 64 MiB equal memory/swap cap exited 0 with OOMKilled false; a 48 MiB hold under a 32 MiB equal cap exited 137 with OOMKilled true (Docker memory limits).

Copy-paste shapes from that lab:

docker run --name mem-ok \
  --memory 64m --memory-swap 64m --cpus 0.25 \
  alpine \
  sh -c 'x=$(dd if=/dev/zero bs=1048576 count=8 2>/dev/null | tr "\0" "A"); echo "bytes:${#x}"'

docker run --name mem-cap \
  --memory 32m --memory-swap 32m --cpus 0.25 \
  alpine \
  sh -c 'x=$(dd if=/dev/zero bs=1048576 count=48 2>/dev/null | tr "\0" "A"); echo "bytes:${#x}"'

How do I set CPU ceilings?

  1. Add --cpus (or Compose cpus / deploy.resources.limits.cpus) on every noisy neighbour so one compile job cannot pin the whole VPS.
  2. Size CPU after memory: a memory OOM is a hard kill; CPU starvation looks like “slowness” without an exit code.
  3. Re-check docker stats CPU% during the job you care about, not only at idle.

How do I size Postgres and other stateful floors?

  1. Cap the database container before you chase shared_buffers.
  2. Trust docker stats, not the SQL GUC, for the idle floor: on 29 September 2026 an idle postgres:16-alpine used 17.34MiB of a 512MiB cap while SHOW shared_buffers still printed 128MB; a second idle container with shared_buffers=32MB used 21.73MiB (Idle Postgres RAM; PostgreSQL docs: resource usage checked 29 September 2026).
  3. Raise the cap from real query load, not from idle.

How do I keep disk from filling?

  1. Measure before delete:
df -h /
docker system df
docker system df -v
  1. Expect three buckets: container logs, image layers, build cache (Docker disk full; Docker docs: docker system df checked 9 October 2026).
  2. Put max-size and max-file on the json-file driver. Docker’s default is no log rotation (Docker docs: configure logging drivers checked 5 October 2026).
  3. Re-create containers after daemon.json log limits. On the 5 October 2026 test VM, daemon restart and docker restart left an old container on {json-file map[]}; only re-create applied max-size:10m and max-file:3 (Docker disk full).
  4. Only-we-have log growth (test VM, 5 October 2026): one container at ~89.32 short lines/s wrote 90,642,495 bytes of log in one hour with no limit; a neighbour with max-size=10m and max-file=3 peaked at 29,854,913 bytes (Docker disk full).
  5. Prune images and build cache only after docker system df shows reclaimable space you accept losing. On 27 September 2026 a shared build host held 749.2 GB of images (555.9 GB reclaimable) and 425.8 GB of build cache (Docker disk full).
  6. On Docker Engine 29 fresh installs, also size /var/lib/containerd — image snapshots may not sit under legacy overlay2 (Docker docs: containerd image store checked 9 October 2026).

What else belongs on the host checklist?

  1. Confirm MemoryLimit=true and SwapLimit=true in docker info before you trust swap-off flags.
  2. Keep Compose files and docker run flags in the same repo as the app so the next deploy does not drop caps.
  3. Separate “install Docker on the VPS” from “rent a VPS”: a container shares the host kernel; a VM keeps its own (Docker or a VPS; Docker docs: what is a container checked 29 September 2026).
  4. Do not expose daemon sockets or common management ports to the public internet.
  5. Note who owns each container before you prune or re-create it.
  6. After changes, re-run the memory job once and re-check docker system df.

Alternatives

Approach When it fits Trade-off
This checklist on one Docker host Few containers, one VPS or homelab You still operate the host
Managed containers / PaaS You want the vendor to own the node Less control; different limits model
Kubernetes LimitRange / ResourceQuota You already run a cluster This page’s docker run flags do not apply

Cons

  • Caps that are too tight kill healthy peak jobs; you must measure the peak, not only idle.
  • Log limits in daemon.json do not retrofit old containers — missed re-creates keep filling disks.
  • Prune commands can delete images you still need if you skip docker system df first.
  • The checklist does not replace backups, TLS, or firewall rules.

Verify

  1. Every important container shows a non-default memory limit in docker inspect.
  2. One deliberate over-cap test (or a documented waiver) exists for the service you care about.
  3. docker system df and df -h / were recorded after log limits and any prune.
  4. Compose (or runbooks) match the live flags.
  5. docker info still shows MemoryLimit and SwapLimit true after any daemon change.

Troubleshooting

Symptom Likely cause Fix
Exit 137, OOMKilled=true Cap below the job Raise memory and swap together, or reduce the job
Host thrash, no OOMKilled Uncapped neighbour or swap confusion Cap neighbours; set equal memory/swap; check SwapLimit
/ full, logs huge No json-file rotation, or limits not re-created Set max-size/max-file, re-create containers
overlay2 path empty on Docker 29 containerd image store Inspect /var/lib/containerd as well

Raw readings

Readings that support other pages, not repeated as claims here: hosted-minute cost tables from the runners tutorial; Cloudflare vs VPS timings; bind-mount vs volume micro-benchmarks. Use those guides when those questions are the ones you have.

Next step

Pick one live container on your VPS today: set equal memory/swap from docker stats, run one real job, and write down OOMKilled and exit code. Then open Docker disk full and apply log limits with a re-create.

Talk to ProDevs if you want us to run the diagnostic on your host: prodevs.in/services/.

Get new ProDevs guides by email

ProDevs Newsletter Form

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top

IP Geolocation by DB-IP