Running Docker in Production
Running a container on your laptop and running one on a live server are two very different things. On your laptop, if a container crashes you just notice and restart it. In production (the real, live environment that your actual users hit), a crashed container at 3am means downtime, and nobody is awake to type docker start. This page walks through the settings and habits that turn a “works on my machine” container into one you can trust on a real Ubuntu server.
Restart policies — keep containers alive
A restart policy tells the Docker daemon (the background service, dockerd, that manages your containers) what to do when a container stops. By default it does nothing, so a crash means your app stays down until you intervene.
The one you almost always want is --restart=unless-stopped. It restarts the container automatically if it crashes or if the whole server reboots, but it leaves the container stopped if you deliberately stopped it. That last part is important: with the alternative --restart=always, a container you intentionally stopped will spring back to life after a reboot, which is rarely what you want.
docker run -d \
--name webapp \
--restart=unless-stopped \
-p 8080:8080 \
myapp:1.4.0
Output:
a3f9c1e2b7d4e8f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4
| Policy | Restarts on crash | Restarts after server reboot | When to use |
|---|---|---|---|
no (default) | No | No | Throwaway/debug containers |
on-failure[:N] | Only on non-zero exit, up to N times | No | Batch jobs that should retry a few times |
always | Yes | Yes (even if you stopped it) | Rarely — surprising behaviour |
unless-stopped | Yes | Yes, unless you stopped it | Most production services |
To change the policy on a running container without recreating it:
sudo docker update --restart=unless-stopped webapp
Resource limits — stop one container eating the server
By default a container can use all the RAM and CPU on the host. One memory leak can then take down every other container and the server itself. You cap this with --memory and --cpus.
--memory=512m— hard memory ceiling. If the app goes over, the kernel’s OOM killer (Out Of Memory killer, the part of Linux that kills processes when RAM runs out) stops the container instead of the whole machine.--cpus=1.5— limits the container to 1.5 CPU cores’ worth of time.
docker run -d \
--name webapp \
--restart=unless-stopped \
--memory=512m \
--cpus=1.5 \
-p 8080:8080 \
myapp:1.4.0
Check live usage with:
docker stats --no-stream
Output:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
a3f9c1e2b7d4 webapp 3.10% 148.2MiB / 512MiB 28.95% 1.2MB / 880kB 0B / 0B
Always set a memory limit on production containers. Without one, a single misbehaving app can freeze the entire Ubuntu host and take your other services down with it.
Logging — rotate logs before they fill the disk
Every line your app writes to stdout/stderr (the standard output and error streams) is captured by Docker’s default json-file log driver and stored on disk under /var/lib/docker/containers/. By default these files never get cleaned up, so a chatty app will silently fill your disk over weeks.
Set a global cap once by editing the daemon config:
sudo nano /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Then restart Docker so it applies to newly created containers:
sudo systemctl restart docker
This keeps at most 3 files of 10 MB each per container (30 MB max). For viewing logs:
docker logs --tail 100 -f webapp
When you outgrow local files, switch the driver to ship logs off the box — for example --log-driver=journald (sends logs to the Ubuntu system journal, readable with journalctl) or a remote collector like Loki or Fluentd.
Run as a non-root user
By default the process inside a container runs as root. If an attacker breaks out of the container, root inside can become a much bigger problem outside. Build images that run as an unprivileged user. In your Dockerfile:
RUN useradd --create-home --uid 10001 appuser
USER appuser
You can also force it at runtime, and drop Linux capabilities you don’t need:
docker run -d \
--name webapp \
--user 10001 \
--read-only \
--cap-drop=ALL \
--security-opt no-new-privileges \
myapp:1.4.0
--read-onlymakes the container’s filesystem read-only (mount a smalltmpfsfor anything that genuinely needs to write).--cap-drop=ALLremoves all special kernel privileges.no-new-privilegesblocks the process from gaining more rights later.
Health checks — know when the app is actually ready
A running container is not the same as a working app — the process can be up while the web server is still hung. A health check is a command Docker runs on a schedule to confirm the app is truly responding.
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -fsS http://localhost:8080/health || exit 1
docker ps then shows (healthy) or (unhealthy), and orchestrators use this signal to restart or stop sending traffic to a bad container.
Use Compose, then an orchestrator
Typing long docker run commands by hand is error-prone. Docker Compose lets you write all of this into one file you can version-control:
services:
webapp:
image: myapp:1.4.0
restart: unless-stopped
ports:
- "8080:8080"
mem_limit: 512m
cpus: 1.5
read_only: true
cap_drop:
- ALL
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
docker compose up -d
When you outgrow plain Docker
Be honest about the limits. Plain Docker (and Compose) runs on one server. It cannot move containers to another machine when that server dies, do zero-downtime rolling updates across hosts, or auto-scale. The moment you need multiple servers, automatic failover, or rolling deploys, you’ve outgrown it — reach for an orchestrator like Kubernetes or the simpler Docker Swarm. Until then, a single well-configured Ubuntu host with Compose is perfectly fine, and far less to operate.
Best Practices
- Use
--restart=unless-stoppedfor long-running services so they survive crashes and reboots without resurrecting on purpose. - Always set
--memoryand--cpusso one container can’t starve the host. - Cap and rotate logs in
/etc/docker/daemon.jsonbefore they fill/var/lib/docker. - Run as a non-root user, drop capabilities, and prefer
--read-onlyfilesystems. - Add a
HEALTHCHECKso Docker and orchestrators can tell “running” from “actually working”. - Keep config in a version-controlled Compose file instead of ad-hoc
docker runcommands. - Move to Kubernetes or Swarm only when you genuinely need multi-host failover or scaling — not before.