Running Docker Productively on a Linux Server
AI generated
$
/etc
Docker · systemd · Monitoring · Linux Server
Running Docker Productively on a Linux Server
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.

16 min read daemon.json · systemd · cgroups · healthchecks Docker 24.x/25.x · systemd · Linux Server

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.

11. FAQ: Running Docker Productively on a Linux Server

1Why does the Docker log file grow without limit and how do I prevent that?
Without configuration Docker uses json-file with no cap. Set max-size and max-file globally in daemon.json, then restart Docker.
2Which storage driver is recommended for production?
overlay2 on modern kernels with ext4 or xfs. devicemapper in loopback mode is unsuitable and no longer supported in current versions.
3What does live-restore do in daemon.json?
Containers keep running through a daemon restart instead of being stopped. Shortens maintenance windows for Docker updates significantly.
4How do I make containers start automatically after a server reboot?
--restart unless-stopped for single containers. For Compose stacks add a dedicated systemd unit file with After=network-online.target.
5Which restart policy is right for production services?
unless-stopped: restarts automatically after a crash and reboot, but stays stopped after an explicit docker stop.
6How do I stop a container from consuming all the server's memory?
Set --memory and --memory-swap, or deploy.resources.limits in Compose. The OOM killer then only terminates the single container.
7What is the difference between HEALTHCHECK and a restart policy?
Restart policy reacts to a terminated process. HEALTHCHECK checks whether the application inside a running container actually works. autoheal couples both mechanisms.
8When should I use named volumes instead of bind mounts?
Always use named volumes for persistent production data. Bind mounts fit development or read-only configuration files.
9Is --privileged ever appropriate in production?
Practically never. Targeted capabilities via --cap-add after --cap-drop ALL cover nearly all legitimate cases more safely.
10How do I monitor multiple Docker hosts centrally?
cAdvisor for container metrics, node_exporter for the host level, both together in Grafana as a central dashboard.