Log rotation, resource limits and healthchecks for continuous operation
Docker containers usually run fine in testing, but in continuous operation log rotation, the right storage driver, clean systemd integration and clear resource limits decide whether a single container brings down the whole server or keeps running quietly in the background. This article shows how administrators configure the Docker daemon for production, keep containers starting reliably and continuously monitor their health status.
Table of Contents
- 1. Why continuous operation needs different rules than testing
- 2. Docker daemon configuration: log rotation and storage driver
- 3. systemd integration: reliable startup and restart
- 4. Resource limits per container: CPU, memory and PIDs
- 5. Docker Compose in production: restart policies and deploy limits
- 6. Monitoring and health checks: watching container state
- 7. Network and volume strategies for persistence and isolation
- 8. Security: non-root containers, capabilities and secrets
- 9. Docker configurations compared directly
- 10. Summary
- 11. FAQ
1. Why continuous operation needs different rules than testing
In a local development environment a container starts with docker run and usually runs for exactly as long as it is needed. In continuous operation on a Linux server different rules apply: containers run for weeks or months, log files keep growing without limit, and a server reboot must not require someone to manually bring important services back up. Running Docker in production means configuring the daemon, container settings and system integration so operation stays stable without constant manual intervention.
Three problem areas show up most often in practice: uncontrolled log growth that fills the disk, a single container with no resource limit that starves the entire host of memory or CPU, and containers that fail to restart automatically after a reboot. The following sections systematically cover how to configure the Docker daemon, systemd integration, resource limits and monitoring so a Linux server running many containers stays reliable in continuous operation.
2. Docker daemon configuration: log rotation and storage driver
Docker's default configuration is built for quick experimentation, not for production. Without explicit log configuration Docker uses the json-file log driver with no size limit: every line of container output keeps growing without bound under /var/lib/docker/containers/<id>/<id>-json.log. A container that logs heavily, say a web server writing its access log to stdout, can push this file to several gigabytes within a few weeks and in the worst case fill the entire root partition. The fix lives in /etc/docker/daemon.json, where max-size and max-file set a global limit for the log driver.
The storage driver choice matters just as much. Since Docker 20.10, overlay2 is the recommended default on all modern Linux kernels with ext4 or xfs, while the older devicemapper driver in loopback mode is explicitly unsuitable for production and is no longer supported in current Docker versions. Every change to daemon.json requires restarting the daemon with systemctl restart docker, which normally stops all running containers unless live-restore is enabled. With live-restore: true containers keep running through a daemon restart, which shortens maintenance windows for Docker updates considerably.
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3",
"compress": "true"
},
"storage-driver": "overlay2",
"live-restore": true,
"default-address-pools": [
{ "base": "172.30.0.0/16", "size": 24 }
],
"userland-proxy": false
}
3. systemd integration: reliable startup and restart
The Docker daemon itself runs as a systemd service (docker.service) and starts automatically at boot once enabled with systemctl enable docker. That alone, however, is not enough to bring individual containers back up after a reboot. Containers started with docker run without a restart policy stay stopped after a server restart, because Docker only remembers the last desired state when a policy such as --restart unless-stopped or --restart always is set. For single containers, the restart policy is therefore the simplest form of systemd integration, since the Docker daemon itself brings up all containers with a matching policy during system startup.
For more complex setups with several related containers, a dedicated systemd unit file that wraps docker compose up and docker compose down and depends explicitly on docker.service is the better approach. That lets a whole Compose stack be placed into the regular boot order, including After=network-online.target, so the stack only starts once the network is actually available. This prevents startup failures for containers that need to reach an external database or DNS name immediately on boot but do not yet have a working network connection.
# /etc/systemd/system/docker-compose-app.service
[Unit]
Description=Application Docker Compose Stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/app
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target
4. Resource limits per container: CPU, memory and PIDs
Without explicit limits, a single container can in theory claim all of the host's memory and every CPU core. That is especially risky on servers running multiple services: a misconfigured container with a memory leak can trigger the kernel's out-of-memory killer, which then does not necessarily terminate the faulty process but rather whatever process the kernel picks, in the worst case the database or the SSH daemon. The --memory and --memory-swap flags on docker run, or the mem_limit and deploy.resources.limits fields in Compose, set a hard ceiling so that only the offending container gets killed by the OOM killer, not the entire host.
Limiting CPU time via --cpus is just as important, allocating a proportional share of CPU cores through cgroups, along with --pids-limit, which caps the number of concurrent processes and threads inside a container and effectively prevents so-called fork bombs. Current Docker installations use cgroups v2, verifiable with docker info | grep -i cgroup. The docker stats command shows live how much CPU, memory and network each container actually consumes, and is the first place to look when diagnosing which container is dragging down a server.
#!/usr/bin/env bash
# Hard limits for a single container: memory, swap, CPU shares and PIDs
docker run -d \
--name magento-worker \
--memory="1g" \
--memory-swap="1g" \
--cpus="1.5" \
--pids-limit=200 \
--restart unless-stopped \
registry.mironsoft.de/magento-worker:latest
# Live resource usage across all running containers
docker stats --no-stream --format \
"table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}"
# Check whether the host runs cgroups v2 (required for --pids-limit accuracy)
docker info | grep -i cgroup
5. Docker Compose in production: restart policies and deploy limits
Docker Compose lets you declare resource limits and restart behavior in YAML instead of repeating flags on every docker run. The deploy.resources.limits key sets the ceiling for CPU and memory, while deploy.resources.reservations defines the minimum guaranteed resources, which matters especially on servers with tight memory for predictable behavior. The unless-stopped restart policy is the right choice for most production services, because it restarts a container automatically after every crash and every server reboot but does not bring it back up if it was stopped explicitly with docker stop.
The healthcheck block in Compose adds an application-specific check on top of plain process monitoring: a container can be running without the underlying service actually answering requests, for example during a long database migration at startup. Using test, interval, timeout and retries defines when a container counts as healthy, and dependent services can wait to start via depends_on: condition: service_healthy until that check has succeeded. This prevents race conditions where an application starts before the database is actually accepting connections.
# docker-compose.yml: production-ready service definition
services:
app:
image: registry.mironsoft.de/magento-app:2.4.8
restart: unless-stopped
deploy:
resources:
limits:
cpus: "2.0"
memory: 2g
reservations:
cpus: "0.5"
memory: 512m
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
depends_on:
db:
condition: service_healthy
networks:
- app-net
db:
image: mysql:8.0
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- db-data:/var/lib/mysql
networks:
- app-net
volumes:
db-data:
networks:
app-net:
driver: bridge
6. Monitoring and health checks: watching container state
The HEALTHCHECK instruction in a Dockerfile or Compose file defines which command runs periodically inside the container to verify application state, usually a simple HTTP request against a /health endpoint or a database ping. The status lands directly in the Docker object and can be read with docker inspect --format='{{.State.Health.Status}}' <container>, with the possible values starting, healthy and unhealthy. This built-in check is the foundation for automated monitoring, because external tools do not need to know themselves how to probe an individual service.
A single healthcheck is not enough for production operation, though: a container marked unhealthy does not restart on its own without additional automation. The companion tool autoheal watches all containers with an active healthcheck through the Docker socket and restarts unhealthy ones automatically. For server-wide monitoring across multiple hosts, cAdvisor exports detailed container metrics in Prometheus format while node_exporter covers the host level. Together in Grafana they give a complete picture of container health, resource consumption and host load.
#!/usr/bin/env bash
# Poll all running containers and alert on unhealthy status
set -euo pipefail
for cid in $(docker ps -q); do
name=$(docker inspect --format='{{.Name}}' "$cid" | sed 's#^/##')
status=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid")
if [[ "$status" == "unhealthy" ]]; then
echo "[ALERT] Container $name is unhealthy, restarting" >&2
docker restart "$cid"
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"Container $name was unhealthy and got restarted\"}"
fi
done
7. Network and volume strategies for persistence and isolation
For persistent data, named volumes (docker volume create, or the volumes field in Compose) are preferable to bind mounts in most cases, because Docker manages them itself, they work independently of the host platform and they do not accidentally collide with host permission issues. Bind mounts remain useful when developers want to mirror source code live into a container during development, but in production they mostly belong where configuration files are mounted read-only. A volume is easy to back up through a temporary helper container that mounts it and writes its contents to an archive on the host via tar.
On the network side, every Compose stack should get its own user-defined bridge network instead of sharing the default network with every other container on the host. User-defined networks provide automatic DNS resolution between containers by service name, so an application container simply reaches the database via db:3306 instead of a fixed IP address. The --network host mode bypasses network isolation entirely and should only be used for very specific cases such as monitoring agents that need access to all host ports, never as the default for application containers.
8. Security: non-root containers, capabilities and secrets
Containers run as the root user inside their namespace by default, which can mean direct root access to the host if an attacker escapes the container isolation mechanism, for example through a kernel vulnerability. The USER instruction in a Dockerfile with an unprivileged UID reduces that risk considerably, as does the --read-only flag, which makes the container's root filesystem immutable and leaves only explicitly declared volumes writable. For applications that need to write temporary files, combining --read-only with --tmpfs /tmp still provides a writable but ephemeral area.
A container's Linux capabilities can be dropped entirely with --cap-drop ALL and then added back selectively with --cap-add only for functions actually needed, for example NET_BIND_SERVICE for processes that need to bind to port 80. The --privileged flag removes all isolation and should practically never be used in production environments. For sensitive credentials such as database passwords, Docker secrets or mounted files are preferable to environment variables, because environment variables are visible via docker inspect and in process lists to any user with Docker access, while secrets are only mounted inside the relevant container.
9. Docker configurations compared directly
Many Docker defaults are built for local development and become a risk in production. The following overview shows the key differences between an unsafe default configuration and the recommended production-ready setting.
| Area | Unsafe default | Recommended production setting | Benefit |
|---|---|---|---|
| Log driver | json-file with no limit | max-size + max-file in daemon.json | Prevents a full root partition |
| Storage driver | devicemapper (loopback) | overlay2 | More stable, officially supported |
| Restart behavior | no --restart set | --restart unless-stopped | Survives reboot and crashes |
| Resources | no limits set | --memory, --cpus, --pids-limit | One container cannot starve the host |
| User inside the container | root (default) | USER with UID + --cap-drop ALL | Reduces damage from a container escape |
In practice, the first two rows of the table have the strongest impact on stability, since unbounded log growth and a missing resource limit are the most common causes of a complete server outage. Fixing these defaults once project-wide, for example centrally in daemon.json and a shared Compose base file, means they do not need to be set again for every new container.
Mironsoft
Docker infrastructure, systemd automation and server hardening for production Linux environments
Docker infrastructure that keeps running through the night?
We analyze existing Docker setups, identify missing resource limits and unsafe defaults, and set up daemon configuration, systemd integration and monitoring so your container infrastructure runs reliably around the clock.
Daemon audit
Configure log rotation, storage driver and resource limits for production
systemd integration
Reliable startup, ordered boot sequence and automatic restart after failures
Monitoring setup
Set up healthchecks, cAdvisor and alerting for container health
10. Summary
Running Docker productively on a Linux server mostly means replacing the convenient defaults built for local development with explicit, production-ready configuration. Log rotation via max-size and max-file in daemon.json prevents a full root partition. The overlay2 storage driver is the stable, officially supported choice. systemd integration through restart policies and dedicated unit files ensures containers and entire Compose stacks reliably come back up after every reboot. Resource limits via --memory, --cpus and --pids-limit make sure a faulty container cannot drag the whole host down with it.
The last, often underrated piece is continuous monitoring: healthchecks in a Dockerfile or Compose file, combined with automatic restarts through tools like autoheal and server-wide metric collection via cAdvisor and node_exporter, turn reactive firefighting into proactive detection. Setting up these five areas cleanly once and folding them into a shared daemon.json and a Compose base configuration means they do not have to be reconsidered for every new container.
Running Docker Productively on a Linux Server, the Essentials at a Glance
Log rotation
Set max-size/max-file in /etc/docker/daemon.json, otherwise json-file fills the root partition.
Resource limits
--memory, --cpus, --pids-limit per container so one cannot starve the entire host.
systemd integration
Restart policy unless-stopped or a dedicated unit file with After=network-online.target.
Monitoring & healthchecks
HEALTHCHECK, autoheal and cAdvisor/node_exporter for automatic detection and response.