Docker Container Security: 9 Settings I Learned the Hard Way
Three weeks. That's how long it took me to fully clean up after a compromised Docker container back in 2023. The attacker got in through a Redis container I'd spun up for testing, forgot about, and left running with default settings for six months.
They mined crypto. They probed my internal network. They used my server to scan other people's infrastructure. AWS sent me a nicely-worded abuse report that started my Saturday off great.
I've since turned container security into a bit of a personal mission. Here are the 9 settings I check on every single container deployment now — whether it's a side project or something handling customer data.
1. Never Run as Root
This is the big one. By default, processes inside a container run as root. If someone breaks out of that container, they have root access on your host (or close to it).
I've seen way too many Dockerfiles that start with FROM node:18 and never set USER. Here's what you should do instead:
FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup . /app
WORKDIR /app
USER appuser
CMD ["node", "server.js"]Alpine images make this easy. If you're using Ubuntu-based images, use useradd -m appuser instead.
One gotcha: if your app needs to bind to a port below 1024 (like 80 or 443), you won't be able to as a non-root user. Use a reverse proxy like Nginx or Caddy that forwards to your app on a high port like 3000.
2. Read-Only Root Filesystem
This one literally saved me when a container got compromised last year. The attacker downloaded a script to /tmp, but my filesystem was read-only so it failed.
docker run --read-only --tmpfs /tmp --tmpfs /var/run my-app:latestThe --read-only flag makes the container's filesystem immutable. You need --tmpfs for directories that require writes, but everything else is locked down.
In docker-compose:
services:
app:
image: my-app:latest
read_only: true
tmpfs:
- /tmp
- /var/run3. Drop All Capabilities, Add Only What You Need
By default, Docker gives containers a bunch of Linux capabilities — CHOWN, DAC_OVERRIDE, FSETID, FOWNER, MKNOD, NET_RAW, SETGID, SETUID, SETFCAP, SETPCAP, NET_BIND_SERVICE, SYS_CHROOT, KILL, AUDIT_WRITE.
Most applications need almost none of these:
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE my-app:latestFor a Node.js API, this is usually sufficient. If you're unsure, run with --cap-drop ALL and watch what breaks in the logs.
4. Set Memory and CPU Limits
I learned this one when a container with a memory leak consumed 12GB of RAM and brought down every other container on the same host.
docker run -m 512m --cpus 0.5 my-app:latestIn docker-compose:
services:
app:
image: my-app:latest
deploy:
resources:
limits:
cpus: '0.5'
memory: 512MDon't skip this. A runaway container can cost you real money on cloud infra — I've seen crypto miners consume $200/day in compute.
5. Mount Secrets Read-Only
docker run -v /host/secrets/db_password:/run/secrets/db_password:ro my-app:latestThe :ro at the end is crucial. If the container gets compromised, the attacker can read secrets but can't overwrite them.
6. Don't Expose the Docker Socket
Mounting /var/run/docker.sock into a container gives it root access to your host. Someone who compromises that container can run any Docker command.
# NEVER do this in production
services:
portainer:
image: portainer/portainer
volumes:
- /var/run/docker.sock:/var/run/docker.sock # 🚩If you must use it, at minimum keep it behind authentication. Better: use Docker's remote API with TLS certificates.
7. Scan Images for Vulnerabilities
I scanned my production images with Trivy and found 47 vulnerabilities — including 3 critical ones.
# Install and scan
trivy image my-app:latest
# High+critical only
trivy image --severity HIGH,CRITICAL my-app:latestTrivy is free and open source. I run it in my CI pipeline — if critical vulnerabilities are found, the build fails.
8. Drop Privileges in Entrypoint
If your entrypoint script needs to run setup tasks, drop privileges before starting the main process:
#!/bin/sh
/app/run-migrations.sh
exec su-exec appuser node server.jsUse su-exec (Alpine) or gosu (Debian) for the privilege drop.
9. Configure Health Checks
A silently crashed container can leave ports open and processes in inconsistent states.
services:
app:
image: my-app:latest
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10sThe One-Command Security Checklist
Here's the command I now run on every new container deployment:
docker run -d \
--name my-app \
--read-only \
--tmpfs /tmp \
--tmpfs /var/run \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
-m 512m \
--cpus 0.5 \
--restart unless-stopped \
-v /host/secrets:/run/secrets:ro \
--health-cmd "wget --spider http://localhost:3000/health" \
--health-interval 30s \
--health-timeout 10s \
--health-retries 3 \
my-app:latestCopy it. Modify it. Use it.
These 9 settings have saved me at least three times since I started taking container security seriously. The crypto miner incident was the wake-up call I needed — hopefully this checklist saves you the same headache.
Got a security setting I missed? Drop it in the comments.