Docker makes self-hosting easy. It also makes it easy to accidentally give an attacker root access to your entire server. Most self-hosting guides skip security entirely — here’s what they don’t tell you.
1. Never Run Containers as Root (When Possible)
By default, processes inside Docker containers run as root. If an attacker escapes the container, they’re root on the host.
Fix: Use the user directive:
services:
myapp:
image: myapp:latest
user: "1000:1000"
Or in the Dockerfile:
RUN adduser --disabled-password --gecos "" appuser
USER appuser
Check your running containers:
docker ps -q | xargs -I {} docker exec {} whoami
If everything says root, you have work to do.
2. Don’t Expose the Docker Socket
The Docker socket (/var/run/docker.sock) gives full control over the Docker daemon — which means full control over the host. Mounting it into a container is like giving that container root SSH access.
Common offenders:
# ⚠️ Dangerous — gives container full host control
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Services that need the socket: Portainer, Traefik, Watchtower, Dozzle.
Safer alternatives:
- Use a socket proxy like tecnativa/docker-socket-proxy:
services:
socket-proxy:
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
CONTAINERS: 1
SERVICES: 0
TASKS: 0
NETWORKS: 0
VOLUMES: 0
networks:
- proxy
traefik:
image: traefik:v3
depends_on:
- socket-proxy
environment:
DOCKER_HOST: tcp://socket-proxy:2375
networks:
- proxy
This exposes only the container listing API — not the ability to create, delete, or execute inside containers.
- Use read-only mounts when a proxy isn’t an option:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
Not as secure as a proxy, but prevents writes.
3. Limit Container Resources
A runaway container can consume all CPU and RAM, taking down your entire server. Set limits:
services:
myapp:
image: myapp:latest
deploy:
resources:
limits:
cpus: '2.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 128M
Or with the older syntax (still works):
mem_limit: 512m
cpus: 2.0
4. Use Read-Only Filesystems
Prevent containers from writing to the filesystem. This blocks many attack vectors:
services:
myapp:
image: myapp:latest
read_only: true
tmpfs:
- /tmp
- /run
volumes:
- app-data:/data # Only this directory is writable
If the app needs to write temporary files, use tmpfs mounts.
5. Drop Unnecessary Linux Capabilities
Docker containers get a set of Linux capabilities by default (like NET_RAW, SYS_CHROOT). Most apps don’t need them:
services:
myapp:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if it needs to bind to ports < 1024
Rule of thumb: Drop everything, then add back only what’s needed.
6. Don’t Use --privileged
The --privileged flag gives a container almost full access to the host kernel. It’s the nuclear option:
# ⚠️ Never do this unless absolutely necessary
privileged: true
When it’s “needed”: Some guides tell you to use --privileged for things like USB passthrough or GPU access. Almost always, there’s a more targeted approach:
# Instead of privileged: true for GPU
devices:
- /dev/dri:/dev/dri
# Instead of privileged: true for USB
devices:
- /dev/ttyUSB0:/dev/ttyUSB0
7. Bind Published Ports to Localhost
When you publish a port, Docker binds it to 0.0.0.0 by default — accessible from the internet:
ports:
- "3306:3306" # ⚠️ MySQL exposed to the world
If the service only needs to be accessed locally or by other containers:
ports:
- "127.0.0.1:3306:3306" # ✅ Only accessible from the host
Better yet: Don’t publish the port at all. Use Docker networks for container-to-container communication:
services:
app:
networks:
- internal
db:
networks:
- internal # No ports published — only 'app' can reach it
networks:
internal:
For more on this, see our guide on checking exposed ports.
8. Keep Images Updated
Old images have known vulnerabilities. Set up automatic updates with Watchtower:
services:
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
WATCHTOWER_SCHEDULE: "0 4 * * *" # 4 AM daily
WATCHTOWER_CLEANUP: "true"
restart: unless-stopped
Check our Watchtower guide for the full setup.
9. Use Docker Secrets for Sensitive Data
Don’t put passwords in environment variables in your compose file — they show up in docker inspect:
# ⚠️ Visible in docker inspect
environment:
DB_PASSWORD: supersecret123
Use Docker secrets or .env files instead:
# .env file (add to .gitignore!)
DB_PASSWORD=supersecret123
services:
db:
environment:
DB_PASSWORD: ${DB_PASSWORD}
Or with Docker secrets (Swarm mode):
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
secrets:
- db_password
10. Network Segmentation
Don’t put all containers on the default bridge network. Create separate networks:
services:
traefik:
networks:
- frontend
- backend
app:
networks:
- backend
- database
db:
networks:
- database # Only accessible from 'app'
networks:
frontend:
backend:
database:
This way, even if Traefik is compromised, the attacker can’t directly reach the database.
Quick Checklist
Run through this for every container:
- Not running as root (or root is necessary and documented)
- No
--privilegedflag - Docker socket not mounted (or using socket proxy)
- Resource limits set
- Published ports bound to localhost (or not published at all)
- Capabilities dropped
- Secrets not in plaintext environment variables
- Image is recent and auto-updating
- On a dedicated network (not default bridge)
Audit Your Setup
Want a quick security check? Run this one-liner to find containers with risky configurations:
# Find privileged containers
docker ps -q | xargs docker inspect --format '{{.Name}}: privileged={{.HostConfig.Privileged}}' | grep true
# Find containers with docker socket mounted
docker ps -q | xargs docker inspect --format '{{.Name}}: {{range .Mounts}}{{.Source}} {{end}}' | grep docker.sock
# Find containers running as root
docker ps -q | xargs -I {} sh -c 'echo "$(docker inspect --format "{{.Name}}" {}): $(docker exec {} whoami 2>/dev/null || echo "unknown")"'
Further Reading
- Check Exposed Ports on Your Server — audit what’s actually accessible from the internet
- Watchtower Setup Guide — keep images updated automatically
- Best Reverse Proxy for Beginners — single entry point to your services
Security isn’t a one-time task. Review your Docker setup monthly, keep images updated, and follow the principle of least privilege: every container should have only the access it absolutely needs.