Docker – Running Containers in Production

August 14, 20265 min readUpdated 8/21/2026

Lesson 17 ended with a list of everything the compose file in this track is not good enough for. This is that list, worked through — which of it Docker can fix, and where you have genuinely run out of Docker.

Restart policies

    restart: unless-stopped

Four values:

no               the default. It stays dead.
on-failure[:5]   restart on a non-zero exit, optionally capped
always           restart whatever happens, including after a daemon restart
unless-stopped   like always, except a container YOU stopped stays stopped

unless-stopped is the usual right answer. The difference from always only shows up on reboot: with always, a container you deliberately stopped last week comes back.

⚠️ A restart policy is not a health policy. It reacts to the process exiting. An application that is deadlocked, or returning 500 to everything, is still running — Docker sees a healthy process and does nothing. A healthcheck marks it unhealthy and still does not restart it; plain Docker has no mechanism for that. Swarm and Kubernetes do.

Resource limits, and the JVM

Without limits, one container can consume the machine and take everything else with it.

    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 1g
        reservations:
          memory: 512m

Memory is a hard ceiling: exceed it and the kernel OOM-kills the process. Exit code 137, lesson 13.

CPU is a throttle rather than a wall — cpus: "1.5" means one and a half cores' worth of time. The container is slowed, not killed, which makes it much harder to notice.

⚠️ Set the memory limit before you tune the JVM, not after. A JVM with no -Xmx sizes its heap from what it believes the machine has. Modern JVMs are container-aware and read the cgroup limit — but only if there is one. With no limit set, the JVM in a 1 GB container sees the host's 32 GB, sizes a heap accordingly, and is OOM-killed long before it ever runs a garbage collection. The container looks like it crashed at random.

-XX:MaxRAMPercentage=75.0

Better than a fixed -Xmx, because it tracks the limit. Leave the remaining 25% for metaspace, thread stacks and native allocations, which are not in the heap and are the thing that usually pushes a "correctly sized" JVM over the edge.

Logs

Log to stdout. Not to a file — a file inside a container is on the writable layer, which dies with it, and is invisible to every collection tool.

Then decide what happens to stdout, because the default is a problem:

    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

The default json-file driver has no rotation at all. A chatty service on a long-running host fills the disk, and the failure this produces is spectacular and confusing — everything on the machine starts failing at once, and nothing points at logs. Those four lines cap it at 30 MB.

Beyond one host you want the logs off the machine entirely — awslogs, gelf, fluentd, or a sidecar collector.

Shutting down without dropping requests

The sequence: Docker sends SIGTERM, waits (10 seconds by default), then SIGKILL.

Getting a clean shutdown needs three things, and all three have to be right.

The signal must reach your process. Exec-form ENTRYPOINT, so your process is PID 1 — lesson 4. Shell form means /bin/sh gets the signal and does not pass it on.

Your application must handle it. Stop accepting new connections, finish in-flight requests, close the pool. Spring Boot:

server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s

The grace period must be long enough.

    stop_grace_period: 30s

⚠️ These two numbers must agree. A 20-second application shutdown behind a 10-second Docker default is SIGKILL every time — and it looks exactly like a graceful shutdown that was configured and did nothing.

⚠️ And a second trap: PID 1 does not get default signal handlers. A process that ignores SIGTERM as PID 1 would be fine anywhere else. If your entrypoint is a shell script spawning children, you also inherit zombie reaping — docker run --init or init: true inserts a minimal init process to deal with both.

Hardening

    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true

Plus USER from lesson 9, which matters more than all of these.

read_only is the one with real teeth and the one most likely to break something — most applications write somewhere, so expect to add tmpfs mounts until it starts. Worth the hour.

no-new-privileges stops a setuid binary inside the container from escalating. Cheap, and there is rarely a reason not to.

Where Docker stops

Be honest about this, because it is the most useful thing in the lesson.

Docker runs containers on one machine. Which means:

That machine is a single point of failure. It dies, everything on it is down. Nothing moves the work elsewhere.

There is no zero-downtime deploy. docker compose up -d stops the old container and starts the new one. In between, requests fail. Rolling updates and health-gated rollouts are an orchestrator feature.

There is no scaling. --scale starts more containers on the same machine, which does nothing for a machine that is already saturated.

Unhealthy is not restarted. As above.

Secrets are environment variables unless you build something better.

None of that means you need Kubernetes. A single well-configured Docker host is a perfectly good answer for an internal tool, a side project, a staging environment, or anything whose users tolerate a few seconds of downtime during a deploy — and it is far simpler to operate than a cluster. Reach for more when you actually have the requirement, not in advance:

one host + compose   simple, cheap, has downtime during deploys
Swarm                multi-host, rolling updates, tiny learning curve, fading
ECS / Cloud Run      managed. No control plane to operate. Cloud-specific.
Kubernetes           everything, at the cost of being a full-time job

The order of things to fix, if you are running containers on one host today: a memory limit and a matching MaxRAMPercentage, log rotation, restart: unless-stopped, USER, and a graceful shutdown whose two timeouts agree. That is most of the value, and none of it requires an orchestrator.

Next: the questions people actually ask.