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:
- Does it run as root? Add a
user:line. - Does it use
network_mode: host? Swap for explicit ports. - 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.