Here's an uncomfortable fact: the app you spun up last night probably runs as root inside its container. That means if someone breaks the app, they get root — and root inside a sloppy container can sometimes climb out onto your whole server.

Beginners never check this. Let's fix that.

Mistake 1: Assuming the container is a sealed box

Containers feel like tiny sealed machines. They're not. A container sharing root with your host is one misconfiguration away from touching your real files. If your docker-compose.yml mounts a folder like /home/you/photos and the app runs as root, a compromised app can rewrite anything in it.

Check it now. Run this on any running container:

docker exec <container-name> whoami

If it prints root, take note. Not every app can drop root, but many can — and most people never try.

Mistake 2: Ignoring the user: line

Most popular images (Nextcloud, Jellyfin, Vaultwarden) support running as a normal user. In Compose, add:

user: "1000:1000"

That's your regular Linux user ID (find it with id -u). The app now runs as you, not root. If it breaks, the damage is limited to what your user can touch — not the whole system.

Some apps complain about file permissions after this. That's the fix working: it's telling you it can no longer write wherever it pleases.

Mistake 3: Giving containers the whole network

Many tutorials say network_mode: host to "make it work." That hands the container your entire network stack. Skip it. Use explicit port mapping instead:

ports:
  - "8080:80"

Now only port 8080 is exposed, and nothing else on your machine is visible to that app.

Mistake 4: Never dropping capabilities

Containers get a pile of Linux "capabilities" they rarely need. Strip them:

cap_drop:
  - ALL
read_only: true

read_only makes the container's own filesystem unwritable except folders you explicitly allow. A hacked app can't drop malware into itself. If the app needs a writable spot, add a tmpfs mount just for that.

Mistake 5: Trusting latest

Using image: someapp:latest means you never know what version you're running. A silent update can break things or pull a compromised build. Pin a real version:

image: someapp:1.4.2

Then update on purpose, after reading the release notes — not by accident at 2 a.m.

The ten-minute audit

Open every Compose file you have. For each service, ask three questions:

  1. Does it run as root? Add a user: line.
  2. Does it use network_mode: host? Swap for explicit ports.
  3. Is the image :latest? Pin a version.

Your one action today: run docker exec <name> whoami on your most exposed app. If it says root, add user: "1000:1000", restart, and fix any permission errors that appear. That single line shrinks your attack surface more than any firewall rule you'll write this month.