IAnswerable

Container Security You Can Actually Do

23 Sep 2026

Most 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.