Docker Security and Operations¶
Why the default is not secure by default¶
Docker's defaults optimize for "it just works," not for "it's locked down." Three defaults specifically need overriding for anything beyond local experimentation:
- Containers run as
rootunless a Dockerfile says otherwise (see Dockerfile Best Practices — run as non-root). - A container's
roothas no resource ceiling unless one is set — a single misbehaving container can consume all of the host's CPU or memory, starving every other container on the same host. - The Docker daemon itself runs as
root, and anyone in thedockergroup can talk to it — which is equivalent to root on the host, becausedocker run -v /:/host ...lets a container mount the host's entire filesystem. Docker group membership is root-equivalent access, not a lesser privilege tier.
Resource limits: set them, don't assume defaults exist¶
docker run --memory=512m --memory-swap=512m --cpus=1.0 my-app
--memory=512m sets a hard ceiling — the kernel's cgroup OOM killer
terminates the container's process if it exceeds this, the same
mechanism described in
Docker Fundamentals.
Setting --memory-swap equal to --memory disables swap for the
container entirely (by default, --memory-swap defaults to double
--memory, silently allowing swap usage that can make an
out-of-memory condition degrade into extreme slowness instead of a
clean, fast OOM-kill — usually the worse outcome operationally, since
a hung container is harder to detect than a killed one).
Without an explicit limit, a container can use however much memory the host has — there is no automatic per-container ceiling. On a shared host running several containers, one leaking process can OOM-kill other containers' processes too, not just its own, once the host's total memory is exhausted (the kernel's OOM killer picks a victim by score, not necessarily the actual offender).
Reduce the attack surface: minimal images, dropped capabilities¶
A container's root user, while confined by namespaces, still has the
full set of Linux capabilities by default (CAP_NET_ADMIN,
CAP_SYS_ADMIN, and dozens more) — most of which a typical web
application never needs, and each of which is a potential escalation
path if the application is compromised.
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-app
--cap-drop=ALL removes every capability, then --cap-add re-adds only
the specific ones actually needed — here, NET_BIND_SERVICE (needed
only to bind a port below 1024; a normal EXPOSE 8080-style app usually
doesn't even need this one). This is the same "deny by default, allow
explicitly" principle as running as non-root,
applied one level deeper.
Scan images for known vulnerabilities¶
docker scout cves my-app:1.0
An image built FROM python:3.12-slim inherits every OS package and
library in that base image — including any CVEs discovered in them
after the image was built. A vulnerability scanner checks the image's
installed package versions against a known-CVE database. This matters
specifically because Docker layer caching (covered in
Docker Fundamentals)
means a base-image layer, once cached, can silently go stale — a
docker build today can reuse a cached python:3.12-slim layer pulled
weeks ago, missing security patches released since, unless the build
explicitly does a docker pull / --no-cache refresh or a CI pipeline
rebuilds base images on a schedule.
Never bake secrets into an image layer¶
# WRONG — this secret is now permanently in the image's layer history
RUN echo "API_KEY=abc123" > /app/.env
Even if a later layer deletes this file, the layer that created it
still exists in the image and is fully extractable — docker history
and docker save both expose every layer's contents, deleted-in-a-later-
layer or not, exactly the same union-filesystem behavior described in
Dockerfile Best Practices — combine RUN instructions.
Secrets belong in runtime environment variables (docker run -e),
mounted secret files (docker run -v, read at startup, never baked
into a layer), or a dedicated secrets manager — never in a COPY or
RUN instruction.
Logs, inspection, and debugging a running container¶
docker logs -f --tail 100 my-container # follow recent logs
docker exec -it my-container sh # shell into a running container
docker inspect my-container # full JSON: mounts, network, env, limits
docker stats # live CPU/memory/network usage per container
docker logs reads from the container's captured stdout/stderr — which
only works if the application actually logs to stdout/stderr rather than
to an internal file the container never exposes. This is why
containerized applications are conventionally written to log to
stdout/stderr rather than to a log file: docker logs (and anything
built on top of the same log driver, like a centralized logging agent)
has nothing to read otherwise.
Common pitfall¶
Running long-lived, stateful debugging sessions via docker exec and
then treating whatever state accumulates there as durable is a trap —
anything created inside a running container that isn't in a mounted
volume disappears the moment the container is removed, exactly like the
writable-layer behavior in
Docker Fundamentals. If a debug
session installs a tool or downloads a file to investigate something,
that installation doesn't survive the next docker compose up rebuild
— it has to either go in the Dockerfile (if it should always be there)
or be treated as genuinely throwaway.