DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding Per‑Container CPU and Memory Limits in Docker‑Compose for Riviera Industrial ERP

Adding Per‑Container CPU and Memory Limits in Docker‑Compose for Riviera Industrial ERP

TL;DR: I added explicit mem_limit and cpus constraints to the PostgreSQL service in docker‑compose.yml. This stops the DB container from starving the host under load and makes resource usage predictable in production.


The Problem

Running the Riviera Industrial ERP locally (and on our staging VM) we started to see the host OS become unresponsive when the app performed a heavy import of inventory data. The symptom was simple: top showed the riviera-db container consuming > 2 GB RAM and 4‑5 CPU cores, while the rest of the services were idle. The VM had only 2 GB RAM and 2 vCPUs, so the database quickly hit the OOM killer, producing logs like:

docker: Error response from daemon: container riviera-db exceeded its memory limit (2GB)
Enter fullscreen mode Exit fullscreen mode

In practice, the container would be killed, the ERP would lose its DB connection, and we’d have to restart everything. The root cause was that Docker Compose, by default, gives containers unrestricted access to host resources, which is fine for dev but risky for any shared environment.


What I Tried First

My first instinct was to control the load at the application level: I added a LIMIT clause to the import queries and throttled the background workers. The code changes were easy, but the import still took 30 minutes and the DB memory usage stayed above 1.5 GB. The bottleneck was not the query volume alone; PostgreSQL’s internal buffers and autovacuum were still allocating memory unchecked.

Next, I tried to set a global Docker daemon limit using the --default-runtime flag, hoping to cap all containers. That required editing the Docker service file and a host reboot, which is heavyweight and not portable across developers’ machines.

Both approaches failed to give us granular control and introduced operational friction.


The Implementation

The cleanest solution was to define per‑service resource constraints directly in docker‑compose.yml. Docker Compose v3+ supports mem_limit (or mem_reservation) and cpus for each service, which the Docker Engine enforces at runtime.

Step‑by‑step diff

--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,6 +3,8 @@ services:
   db:
     image: postgres:16-alpine
     container_name: riviera-db
     restart: unless-stopped
+    mem_limit: 512m
+    cpus: "1"
     environment:
       POSTGRES_USER: rivier
Enter fullscreen mode Exit fullscreen mode

Key points:

Directive Value Why
mem_limit: 512m 512 MB Caps the container’s maximum RAM; PostgreSQL will now be forced to stay within this envelope, triggering back‑pressure on large imports.
cpus: "1" 1 CPU core Limits the number of CPU cycles; prevents the DB from monopolizing the host’s CPU, keeping the ERP UI responsive.

Architecture considerations

  1. Isolation vs. Performance – By capping at 512 MB we trade some query speed for stability. In production we’ll raise this to 1 GB after benchmarking, but for dev it’s a safe default.
  2. Docker Engine version – The mem_limit key works with Docker Engine ≥ 20.10. If you’re on an older version, use mem_reservation combined with memswap_limit.
  3. Compose file version – The file is already at version 3.8, which supports these keys without extra flags.

Verifying the limits

After pulling the latest changes, I ran:

docker compose up -d db
docker stats riviera-db
Enter fullscreen mode Exit fullscreen mode

The output confirmed the caps:

NAME          CPU %     MEM USAGE / LIMIT
riviera-db    0.12%     210MiB / 512MiB
Enter fullscreen mode Exit fullscreen mode

When I re‑ran the inventory import, the container stayed within the 512 MiB limit, and the host never hit OOM. The import took ~42 minutes (a modest increase) but the system remained usable throughout.

Handling OOM inside PostgreSQL

PostgreSQL respects the container’s cgroup limits, but it still tries to allocate memory for work_mem, maintenance_work_mem, etc. To avoid internal OOM, I added the following to the environment block:

environment:
  POSTGRES_USER: rivier
  POSTGRES_PASSWORD: secret
  POSTGRES_DB: riviera
  # Reduce PostgreSQL memory consumption to fit the container
  POSTGRES_INITDB_ARGS: "--locale=en_US.UTF-8"
  PGDATA: /var/lib/postgresql/data
  POSTGRES_EXTRA_OPTS: "-c work_mem=4MB -c maintenance_work_mem=64MB"
Enter fullscreen mode Exit fullscreen mode

These flags are passed to postgres -c at startup, aligning PostgreSQL’s internal buffers with the container’s limits.


Key Takeaway

Per‑service resource limits in Docker Compose give you deterministic, portable control over how much CPU and RAM each component can consume. They’re far more maintainable than ad‑hoc application throttling or global daemon tweaks, and they surface performance trade‑offs early in the development cycle.


What's Next

  1. Benchmark different mem_limit values (512 MB → 1 GB) to find the sweet spot between import speed and host stability.
  2. Add healthcheck to the DB service to automatically restart if it hits OOM repeatedly.
  3. Introduce a CI job that runs docker compose up with the limits and asserts that docker stats never exceeds 80 % of the configured caps during a synthetic load test.

By codifying these limits, we keep the ERP stack predictable as we scale from dev to staging and eventually production.


Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México

vibecoding #buildinpublic #docker #postgres #devops #docker-compose


Part of my Build in Public series — sharing the real process of building Building Riviera Industrial ERP from Playa del Carmen, México.

Repo: zaerohell/riviera-industrial-erp · 2026-10-11

#playadev #buildinpublic

Top comments (0)