Container Security You Can Actually Do
23 Sep 2026Most containers on most VPSes run as root, with a writable filesystem, full Linux capabilities, and no resource limits. None of that is necessary for a typical web app. Fix these four things and you’ve closed the gaps that actually get exploited.
1. Don’t run as root
The default user in most base images is root. If an attacker gets code execution inside the container, root inside it makes privilege escalation and further exploitation easier — and if there’s any container-runtime bug, root inside maps closer to root outside.
In your Dockerfile:
RUN useradd --uid 1000 --create-home appuser
USER appuser
Or at runtime, without touching the image:
docker run --user 1000:1000 myapp
Check what a running container is actually using:
docker exec mycontainer whoami
If that prints root, fix it before anything else on this list.
2. Make the filesystem read-only
Most apps don’t need to write to their own container filesystem. If they can’t write, a huge class of attacks — dropping a webshell, modifying a binary, persisting malware — just doesn’t work.
docker run --read-only --tmpfs /tmp myapp
The --tmpfs /tmp gives it a writable scratch space for anything that genuinely needs temp files, without persisting anything to disk. If the app breaks, it’s telling you where it writes — mount exactly that path as a volume or tmpfs, nothing more.
3. Drop capabilities you don’t need
Docker containers get a default set of Linux capabilities — things like CAP_NET_RAW (raw sockets, used for ping-style attacks) and CAP_CHOWN — most apps never touch. Drop everything, then add back only what’s needed:
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp
NET_BIND_SERVICE is the common one you’ll need back if the app binds a port below 1024 as a non-root user. Most services need nothing else.
4. Set resource limits
Without limits, one container with a memory leak or a runaway loop can take down everything else on the host — including other containers and the host’s own processes if it triggers the OOM killer indiscriminately.
docker run --memory 512m --cpus 1 myapp
Pick numbers based on what the app actually needs under load, not a guess. Watch docker stats for a day before tightening these on something already in production.
Putting it together
docker run \
--user 1000:1000 \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--memory 512m \
--cpus 1 \
myapp
Failure modes to expect
Dropping to a non-root user breaks images that assume they can write to /app or install packages at startup — you’ll see permission denied errors in the logs. Fix the image, don’t add the capability back.
--read-only breaks anything that logs to a local file inside the container instead of stdout — which it should be doing anyway in a containerized setup. Redirect logs to stdout/stderr and let your log driver handle collection.
The one to skip for now
Seccomp and AppArmor profiles are real hardening, but they’re also where people burn a weekend tuning a custom profile for marginal gain over Docker’s solid default seccomp profile. Ship the four changes above first — they’re an afternoon, not a weekend, and they cover the attacks you’re actually likely to see.