Docker Compose Resource Limits for a Small VPS

Docker Compose Resource Limits for a Small VPS

Set Docker Compose resource limits for memory, CPU, and processes. Protect a small VPS from noisy containers, verify the settings, and avoid restart loops.

đź’ˇ Disclosure: This article contains affiliate links. If you make a purchase through these links, we may earn a small commission at no extra cost to you. This helps support the site and keeps the content free.

One container should not get to ruin your entire VPS. Docker Compose resource limits put boundaries around memory, CPU, and process counts so a noisy application has less room to hurt its neighbors.

My opinion: setting those boundaries is more useful than adding another dashboard and hoping someone watches it. Monitoring tells you something is wrong. Limits can reduce the blast radius before you open the alert.

This is a short guide for regular Docker Compose on a Linux server, not a Swarm deployment. We’ll use a disposable Alpine container to check the settings before applying the same pattern to a real application.

What Docker does without limits

By default, Docker containers have no resource constraints. A container can compete for the resources that the host kernel makes available, including memory that your database and SSH session also need.

CPU contention usually makes things slow. Memory pressure can be nastier: Linux may kill processes when it cannot satisfy allocations. Your host is not automatically protected just because the troublesome app lives in a container.

I prefer an explicit ceiling for background jobs and noncritical apps. For databases, photo processing, and Java services, I would first measure their workload rather than copy a random memory number from a blog. That includes this blog.

Start with a disposable Compose project

You need Docker Engine and the current Docker Compose plugin. Run this on a test host or a server with enough spare capacity for a tiny container; it does not publish ports or mount your application data.

Create a directory named resource-limit-demo and save the following as compose.yaml inside it:

services:
  demo:
    image: alpine:3.23
    command: ["sleep", "3600"]
    mem_limit: 256m
    mem_reservation: 128m
    memswap_limit: 256m
    cpus: 0.50
    pids_limit: 64
    restart: "no"

The image is just a demonstration workload, not a recommendation for your app’s base image. There is no database here and nothing worth preserving. That makes it a better place to learn than your only production stack.

Validate and start it from that directory:

docker compose config --quiet
docker compose up -d

If your kernel does not support a requested constraint, Docker may warn or reject the configuration. Read those warnings instead of treating a successful YAML parse as proof of enforcement.

What those five settings actually mean

mem_limit sets a hard memory limit. At that boundary, an allocation may fail or the kernel may kill a process inside the container. This is containment, not a graceful recovery mechanism.

mem_reservation is a softer memory setting used under contention. It is not a promise that Docker reserves that much physical RAM for the service. Keep it below the hard limit, and do not use it as a replacement for one.

memswap_limit is easy to misread. It describes the combined memory-and-swap allowance, not a separate swap budget. Setting it equal to mem_limit, as above, prevents the container from using swap when the host supports swap accounting.

I choose that behavior for this demo because it makes the boundary simple. In production, swap can provide breathing room, but swapping a latency-sensitive database is not a free performance upgrade. Make that decision deliberately.

cpus: 0.50 caps the container at half a CPU’s worth of execution time through CPU quota scheduling. It does not dedicate half a physical core or pin the container to a specific core. The trade-off is throttling: a busy job can take longer, and requests can become slower.

pids_limit limits tasks, including threads, in the container’s process control group. A low value can stop an application from creating workers even when CPU and RAM look fine. The example’s limit is suitable for a sleeping demo, not a universal setting for JVMs or database servers.

Verify Docker received the limits

A configuration file expresses intent. Inspecting the running container checks what Docker actually received.

id=$(docker compose ps -q demo)
docker inspect "$id" --format '{{json .HostConfig}}'

In the returned JSON, check Memory, MemoryReservation, MemorySwap, NanoCpus, and PidsLimit. Docker reports memory in bytes and CPU quota through its internal representation, so do not expect the original strings from the YAML.

For the memory setting in this example, Memory and MemorySwap should both be 268435456. The reservation should be 134217728, NanoCpus should be 500000000, and PidsLimit should be 64.

Watch current usage too:

docker stats --no-stream "$id"

This confirms visibility into the running container. It does not demonstrate how your real app behaves under load. The sleeping demo will not tell you whether an import job, startup migration, or media scan can survive the same limits.

Apply the pattern to one real service

Add the resource fields under the service you want to constrain in your existing Compose file. Keep its image, volumes, environment variables, and networking intact. Do not replace your app’s complete definition with the demo.

Start with a noncritical service, observe normal use and its busiest operation, then choose limits with headroom. A steady-state memory reading is not the full story: startup and scheduled work can be substantially different workloads.

Validate the complete configuration before applying it:

docker compose config --quiet
docker compose up -d --no-deps --force-recreate YOUR_SERVICE

Replace YOUR_SERVICE with the actual Compose service name. Recreating a container interrupts that service, so use a maintenance window when necessary. Persistent data must already live in correctly configured volumes or bind mounts; do not rely on the old container’s writable layer.

I would not restart the Docker daemon just to add these service-level settings. A targeted recreation keeps the change scoped to the application you are tuning. If you already use deploy.resources, keep overlapping values consistent rather than adding contradictory limits.

Our Compose validation guide covers checks before deployment. Docker healthchecks can help detect an app that is running but no longer serving requests.

Don’t turn a memory limit into a restart loop

A restart policy does not make an undersized memory limit correct. If the app is killed and immediately restarted, it may repeat the same expensive startup until your dashboard resembles a Christmas tree.

Check the state and logs after the change:

id=$(docker compose ps -aq YOUR_SERVICE)
docker inspect "$id" --format '{{json .State}}'
docker compose logs --tail=100 YOUR_SERVICE

Look for OOMKilled, the exit code, and the application’s own errors. Exit code 137 alone is not proof of an out-of-memory kill: other forced termination paths can produce it too. Correlate the evidence rather than diagnosing from one number.

If a limit causes failures, raise it or roll back the change, then investigate the workload. Do not disable the OOM killer as a shortcut. A limit that continually breaks the app is not a successful hardening measure.

Limits are not capacity planning

Each container’s ceiling is independent. Docker does not reject a regular Compose stack simply because all your memory limits add up to more RAM than the host has.

Leave room for Linux, Docker, filesystem activity, and anything running outside containers. Watch simultaneous peaks, not just one quiet screenshot. If everything needs its maximum at the same time, tighter YAML cannot manufacture more memory.

These constraints also do not bound application data, log storage, or network traffic. Keep monitoring and backups in place. Resource limits are one layer, not an excuse to ignore the others.

A separate note on remote administration

Resource isolation and network privacy solve different problems. Use SSH or a private mesh VPN to reach your own server; a commercial VPN subscription is not a substitute for that access-control design.

If you also want a commercial VPN for your administrator laptop on untrusted networks, the affiliate option below is separate from this tutorial. You do not need it to configure Docker limits.

🚀NordVPN

An optional VPN for your administrator laptop on untrusted networks, not a replacement for private server access.

Get NordVPN →

Affiliate link — we may earn a commission at no extra cost to you.

Your next move

Remove the disposable demo from its directory when you are done:

docker compose down

Then choose one noncritical service, measure its busy period, add sensible limits, and inspect the recreated container. That’s a useful Friday maintenance task: one small change, a visible boundary, and a rollback you understand.

For the precise semantics and platform caveats, read Docker’s resource constraints documentation and the Compose services reference. Trust the running configuration, not just a tidy YAML file.

Stay in the loop 📬

Get self-hosting tutorials, tool reviews, and infrastructure tips delivered to your inbox. No spam, unsubscribe anytime.

Join 0 self-hosters. Free forever.