Running Docker in production means more than keeping containers up. A single noisy process, an unbounded log file, or years of unused image layers can destabilize the whole VM even when every container is otherwise configured correctly.
The practical goal is simple: give important containers explicit resource boundaries, make log growth predictable, and treat Docker disk usage as an operating metric instead of an emergency.
This guide owns those production mechanics. It complements Docker on a Cloud VM, which defines the overall host architecture, and Docker Host Security Checklist for Production VMs, which owns privilege and exposure controls. For persistent application data, use Docker Volumes vs Bind Mounts.
Docker resource limits are host stability controls
Docker containers do not receive automatic CPU or memory ceilings just because they run in separate containers.
By default, a container can use as much CPU or memory as the host kernel allows. That is convenient during development, but it can turn one runaway process into a host-wide incident in production.
A useful production model separates three questions:
| Question | Control |
|---|---|
| How much memory can this container consume? | Memory limit and optional reservation |
| How much CPU can this container use? | CPU quota or CPU share policy |
| How much spare capacity should remain for the host? | VM-level headroom |
The host always needs capacity for more than the application containers. Docker Engine, the operating system, SSH, monitoring agents, filesystem cache, backup jobs, package updates, and short traffic bursts all compete for the same resources.
A container limit is therefore not only about fairness between containers. It protects the host boundary itself.
A production VM should avoid both extremes:
- No limits anywhere: one service can consume the whole machine.
- Limits set exactly at idle usage: ordinary traffic bursts cause throttling or OOM failures.
The better model is measured limits plus host headroom.
Memory limits should reflect failure tolerance, not guesses
Memory pressure behaves differently from CPU pressure.
CPU overuse normally causes throttling and slower work. Memory exhaustion can cause process termination. On Linux, if the host reaches severe memory pressure, the kernel's out-of-memory system may kill processes to recover capacity.
Docker documents hard and soft memory controls:
--memorycreates a hard ceiling;--memory-reservationcreates a softer target used during contention;- swap behavior can be controlled separately;
- Docker recommends leaving the OOM protections in place rather than disabling them casually.
A practical policy is:
| Workload | Memory approach |
|---|---|
| Stateless API or worker | Use a measured hard limit with restart and monitoring |
| Cache | Set an application-level cache limit and a container limit |
| Build job | Allow burst capacity, but prevent it from consuming the host |
| Database | Size deliberately and avoid tight limits that conflict with database memory behavior |
| Reverse proxy | Usually small, but still monitor for unexpected growth |
The most dangerous memory configuration is often not a limit that is too low. It is an important service with no known memory budget at all.
OOM behavior should be observable
If a container reaches its memory ceiling, the operator needs to know whether the failure came from:
- a legitimate traffic spike;
- a memory leak;
- a bad deployment;
- an undersized limit;
- an undersized VM;
- another container creating host-level pressure.
Treat OOM events as capacity signals. Repeatedly raising limits without understanding the cause only moves the failure point.
Avoid disabling the OOM killer as a general reliability tactic. Docker specifically warns that disabling OOM handling without a memory limit can allow a container to create host-wide memory exhaustion.
Swap is a buffer, not a replacement for RAM
Swap can prevent a short burst from immediately exhausting physical memory, but it changes performance characteristics.
A container that repeatedly pushes active application memory into swap may remain alive while becoming dramatically slower. That can be worse than an explicit failure for latency-sensitive services.
Docker's memory controls allow several models:
- no explicit container memory limit;
- memory limit with additional swap allowance;
- memory limit with swap disabled for that container;
- unlimited container swap up to host availability.
There is no universal production value. The right choice depends on the workload.
Use swap conservatively for:
- short non-latency-sensitive bursts;
- background jobs;
- hosts where a small emergency buffer is preferable to immediate OOM.
Be more cautious for:
- databases;
- low-latency APIs;
- caches;
- workloads where swap thrashing would create cascading timeouts.
The main rule is to know whether swap is part of the operating design. Do not let it become an invisible performance fallback.
CPU limits prevent noisy neighbors without creating false guarantees
CPU is generally easier to constrain than memory because saturation usually slows work rather than killing it.
Docker's --cpus option is the simplest hard ceiling for many workloads. A value of 0.5 allows up to half of one CPU, while 1.5 allows up to one and a half CPUs worth of scheduling time.
CPU shares are different. They influence relative priority when CPU is contended, but they are not a strict reservation when the host is otherwise idle.
That creates two useful patterns:
| Goal | Better control |
|---|---|
| Prevent a background job from monopolizing the host | Hard CPU limit |
| Let several services share spare CPU but prioritize one | CPU shares / relative weight |
| Pin a specialized workload to specific cores | CPU set, only when needed |
| Guarantee real-time behavior | Advanced kernel scheduling, not a normal Docker default |
For most small teams, explicit cpus values are easier to reason about than low-level scheduler tuning.
The key production mistake is confusing a CPU limit with capacity planning. A container limited to one CPU can still saturate that CPU continuously. The limit protects neighbors; it does not prove the service has enough compute.
Compose should make resource policy visible
For multi-service applications, resource policy belongs in the same versioned deployment model as the services themselves.
Current Docker Compose supports service-level CPU and memory controls, including cpus, mem_limit, memory reservation, swap settings, PID limits, and deployment resource limits.
A small production example might look like:
services: app: image: example/app:1.4.2 cpus: 1.5 mem_limit: 1g mem_reservation: 512m worker: image: example/app:1.4.2 command: worker cpus: 0.75 mem_limit: 768m
The exact values are workload-specific. The important part is that the limits are visible, reviewable, and changed intentionally alongside the application configuration.
Do not copy resource values from another stack merely because the services use the same language or framework. Two Node.js, Python, or Java applications can have completely different memory and CPU profiles.
Use observed production behavior to set limits, then leave enough room for bursts and host services.
Logging is a disk-management decision
Container logs are often the quietest path to a full filesystem.
Docker's default logging driver is json-file. Current Docker documentation explicitly notes that it does not rotate logs by default, which means a chatty container can grow its log file until the host runs out of disk space.
That makes logging policy part of capacity planning.
A production logging decision should answer:
- Which log driver is used?
- How much local log history can each container retain?
- Is rotation enabled?
- Are logs compressed?
- Are important logs also sent off-host?
- How long must logs remain searchable?
- Who alerts on abnormal log volume?
A useful small-team default is to prefer Docker's local logging driver when local Docker logs are enough. Docker recommends local for ordinary Engine use because it rotates logs automatically and uses a more space-efficient format than unbounded json-file logging.
By default, the local driver keeps up to five 20 MB files per container before compression, or roughly 100 MB of uncompressed log history per container.
That is a bounded default, not a universal retention target.
json-file can still be safe when rotation is explicit
Some teams keep json-file because tooling already expects it. That is valid if rotation is configured.
For example, Docker supports a daemon-level configuration such as:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
This caps the retained local log set instead of allowing a single file to grow without a ceiling.
Two operational details matter:
- Changing the Docker daemon's default logging configuration affects newly created containers, not existing containers automatically.
- Logging options in
daemon.jsonare expressed as strings.
That means a logging-policy change is incomplete until the affected containers have been recreated with the intended configuration.
Do not assume that restarting Docker alone retrofits existing containers.
Remote logging changes retention, not host responsibility
Sending logs to a remote system can reduce dependence on local disk and improve incident analysis, but the Docker host still needs a clear local policy.
Remote logging can fail because of:
- network outages;
- authentication problems;
- remote service limits;
- configuration mistakes;
- backpressure or rate spikes.
If your remote logging driver uses Docker's local cache for docker logs, that cache also has its own disk behavior.
The architecture decision is therefore:
Application stdout/stderr ↓ Docker logging driver ├── bounded local retention └── optional remote aggregation
Do not treat remote logging as permission to ignore disk alerts on the Docker host.
Docker disk usage comes from several different stores
When a Docker VM fills its disk, the cause is not always container logs.
Common consumers include:
- image layers;
- stopped containers;
- writable container layers;
- named and anonymous volumes;
- bind-mounted application data;
- build cache;
- local container logs;
- database files;
- application uploads;
- backup archives stored on the same VM;
- operating-system logs and package cache.
Docker's docker system df command shows disk usage managed by the Docker daemon, including images, containers, local volumes, and build cache.
A more detailed review uses:
docker system df -v
This is a much better starting point than deleting files manually from Docker's data directory.
For individual running-container writable layers, docker ps -s can help identify containers that are accumulating unexpected filesystem changes.
Large writable layers often indicate an architectural problem, such as:
- logs being written inside the container instead of through the logging driver;
- uploads written to the container layer instead of persistent storage;
- temporary files never being cleaned;
- package or build artifacts created at runtime;
- application data stored in the wrong path.
Containers should remain reasonably replaceable. Persistent data belongs in a deliberate storage path.
Images and build cache should be managed, not routinely wiped
Docker does not aggressively remove unused objects automatically. That conservative behavior prevents accidental deletion but also means old objects can accumulate.
Normal sources of growth include:
- repeated application builds;
- multiple retained image tags;
- old stopped containers;
- BuildKit cache;
- temporary CI images;
- one-off debugging containers.
This is not automatically waste. Build cache can make deployments much faster, and retaining a previous image can make rollback easier.
The goal is not to keep Docker at minimum disk usage. The goal is to know which disk usage is intentional.
Use a simple classification:
| Docker object | Keep when | Review when |
|---|---|---|
| Current image | Running or rollback target | Never remove blindly |
| Previous release image | Needed for rollback window | Remove after policy expires |
| Dangling image | Rarely needed | Safe candidate after review |
| Build cache | Speeds active builds | Trim when excessive or stale |
| Stopped container | Needed for debugging | Remove after investigation |
| Named volume | Contains intentional state | Never prune without ownership check |
| Anonymous volume | Temporary or accidental state | Review before cleanup |
Disk cleanup should follow ownership, not age alone.
docker system prune is useful but intentionally broad
docker system prune removes unused Docker objects such as stopped containers, unused networks, dangling images, and unused build cache.
That makes it useful during controlled cleanup, but it should not be treated as a cron job that blindly runs on every production host.
Docker intentionally does not remove volumes by default with docker system prune, because unused volumes may contain important data.
Adding broader flags increases the blast radius:
-aalso removes unused tagged images, not only dangling ones;--volumescan remove unused anonymous volumes;- separate volume-prune commands can remove named volumes when explicitly requested.
Before pruning production systems, ask:
- Which images are required for rollback?
- Are stopped containers still needed for debugging?
- Which volumes contain state?
- Is build cache intentionally retained for deployment speed?
- Can the deleted object be recreated safely?
A safer production habit is inspect first, prune second.
Volumes need a different cleanup policy from images
Image cleanup and volume cleanup should not share the same automation policy.
Images are generally reproducible artifacts. Volumes may contain primary data.
That means this command category:
image prune container prune build cache prune
has a fundamentally different risk profile from:
volume prune
If a named volume exists without an attached running container, that does not mean it is disposable. It may belong to a stopped database, a migration, a rollback path, or a workload waiting to be recreated.
Every production volume should have an owner and purpose.
For the persistence decision, use Docker Volumes vs Bind Mounts. This guide only owns operational disk pressure and cleanup policy.
Disk alerts should trigger before Docker reaches emergency state
A full filesystem can cause failures that look unrelated to storage:
- databases stop accepting writes;
- containers fail to start;
- deployments fail while extracting images;
- log writes fail;
- package upgrades fail;
- applications return unexpected errors;
- monitoring agents stop recording data.
The host should alert before it reaches that point.
A useful monitoring set includes:
- root filesystem utilization;
- Docker data-root utilization if separate;
- rate of disk growth;
- inode usage;
- Docker image and build-cache growth;
- per-volume growth for important data;
- application-specific data growth;
- abnormal container log rates.
Do not pick one percentage threshold and assume it fits every VM. A host with 20 GB free and a database growing 5 GB per hour is in more danger than a host with 10 GB free and stable usage.
Monitor both remaining capacity and growth rate.
Production headroom should be deliberate
Resource limits are most effective when the VM itself has spare capacity.
If the sum of all container hard limits equals the entire VM capacity, the host has no room for:
- Docker Engine;
- filesystem cache;
- monitoring;
- SSH sessions;
- backup jobs;
- deployments;
- short application bursts;
- kernel memory;
- operating-system processes.
Likewise, a disk that is intentionally filled to 95% in normal operation leaves little space for image pulls, database maintenance, temporary files, and log bursts.
Think in terms of three layers:
VM capacity ├── operating-system + Docker reserve ├── normal container demand └── burst / maintenance headroom
The correct percentages depend on the workload. The principle does not.
A practical production baseline
Use this baseline before treating a Docker VM as production-ready:
| Area | Production baseline |
|---|---|
| Memory | Important containers have measured limits or a documented reason not to |
| CPU | Noisy background services cannot monopolize the host |
| Swap | Behavior is intentional and monitored |
| OOM | OOM events are visible and investigated |
| Compose | Resource policy is versioned with the service definition |
| Logging | Log growth is bounded locally |
| Logging driver | local, rotated json-file, or deliberate remote driver |
| Disk usage | docker system df is part of routine review |
| Writable layers | Unexpected growth is investigated |
| Images | Rollback images retained intentionally; stale artifacts reviewed |
| Build cache | Retained for speed but bounded by policy |
| Volumes | Never deleted only because Docker reports them unused |
| Pruning | Manual or controlled automation with understood scope |
| Alerts | Capacity and growth are monitored before emergency thresholds |
| Host headroom | Capacity remains for OS, Docker, maintenance, and bursts |
This is a stronger production model than simply scheduling a weekly docker system prune.
Raff VM sizing should follow measured aggregate demand
On a Raff VM, Docker containers share the CPU, memory, storage, and network resources available to the guest operating system.
The best sizing workflow is therefore:
- observe normal and peak container usage;
- apply sensible service-level limits;
- preserve headroom for the host and maintenance activity;
- monitor growth over time;
- resize the VM when the aggregate workload consistently approaches the intended operating range.
Avoid sizing a VM from a fixed “containers per server” rule. A single database or build container can use more resources than dozens of lightweight web services.
The same applies to disk. Image count alone does not tell you whether the host is safe. Logs, volumes, uploads, database growth, and build cache all matter.
Use the live Raff VM page when selecting current compute capacity.