Docker security for self-hosting is not optional — it’s the difference between a stable homelab and a compromised one. Home servers running Jellyfin, Vaultwarden, Nextcloud, or Immich are increasingly scanned by automated bots within minutes of being exposed online. The good news: most attack vectors are preventable with a handful of configuration changes. This guide walks you through 7 proven practices — rootless containers, image scanning, secrets management, network isolation, capability dropping, resource limits, and automated updates — so you can lock down your stack without breaking anything.
Why Docker Defaults Are Not Enough
Docker ships with sensible defaults for development environments, not production or homelab exposure. Out of the box you get:
- 🔴 A daemon running as root
- 🔴 All containers on a shared
bridgenetwork (they can reach each other by default) - 🔴 No image vulnerability checks
- 🔴 Full Linux capabilities inside containers
- 🔴 No memory or CPU limits
These defaults are fine on a laptop you never expose. The moment you open a port, reverse-proxy a service, or run a VPN, Docker security for self-hosting becomes a real attack surface. The practices below address each of these gaps directly. Treating Docker security for self-hosting as a one-time setup rather than an ongoing checklist is where most people go wrong.
Docker Security for Self-Hosting: The Core Checklist ✅
This checklist applies whether you run containers on a Proxmox LXC, a bare Debian host, or a NAS running OpenMediaVault. Pick the items that fit your setup and implement them in order.
1. 🔒 Run Containers Rootless
By default, the Docker daemon runs as root. A container escape — even a partial one — gives an attacker host-level root access. Rootless Docker moves the entire daemon into user space, so a compromised container never reaches the host’s root.
Follow the official Docker rootless setup guide:
dockerd-rootless-setuptool.sh install
Add to your shell profile:
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
⚠️ Critical: Rootless mode has real limitations. Ports below 1024 require sysctl net.ipv4.ip_unprivileged_port_start=80, and some storage drivers behave differently. Test thoroughly before switching production services.
If full rootless isn’t feasible yet, at minimum force a non-root user in your Compose file:
services:
myapp:
image: myapp:latest
user: "1000:1000"
This single line eliminates a broad class of privilege escalation risks and is one of the most underused Docker security for self-hosting improvements.
2. 🔍 Scan Images Before Deployment
You cannot trust images blindly — even official ones occasionally ship with known CVEs. Scanning every image pull is a core Docker security for self-hosting habit that takes 30 seconds and can save hours of incident response.
Trivy is the best open-source option:
trivy image nginx:latest
It checks OS packages, application dependencies, misconfigurations, and embedded secrets. Install on Debian/Ubuntu:
sudo apt install trivy
Docker Scout is built into the Docker CLI if you prefer a vendor-supported tool:
docker scout cves nginx:latest
Run scans before every docker compose up with a new image version. Automate it — see the CI/CD section below.
3. 🗝️ Use Secrets, Not Plain-Text Env Vars
Storing passwords in .env files or directly in docker-compose.yml is the most common pitfall in Docker security for self-hosting. Anyone with read access to those files — or to a leaked Git repo — gets your credentials.
Use file-based secrets instead:
services:
db:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
secrets:
- db_root_password
secrets:
db_root_password:
file: ./secrets/db_root_password.txt
Create the secrets folder with tight permissions:
mkdir -m 700 ./secrets
echo "yourpassword" > ./secrets/db_root_password.txt
chmod 600 ./secrets/db_root_password.txt
Add ./secrets/ to .gitignore. This pattern works in standalone Compose — you don’t need Docker Swarm. For a real-world example of a container that handles credentials properly, see how I set up Vaultwarden in Docker.
4. 🌐 Isolate Networks Between Containers
By default, every container on a Docker host can reach every other container on the default bridge network. In Docker security for self-hosting this is a serious silent risk: a compromised Jellyfin or Nextcloud container can probe your database, your password manager, or any other service on the same host.
Fix it with explicit custom networks:
networks:
frontend:
backend:
services:
nginx:
networks:
- frontend
app:
networks:
- frontend
- backend
db:
networks:
- backend
The db container is now completely unreachable from the nginx container. Only app can talk to both. 🧱
Also: never use network_mode: host unless absolutely required. It bypasses all Docker network isolation and puts the container directly on your LAN interface. For checking subnet overlaps, the IP Subnet Calculator on this site can help you plan your Docker networks.
5. 📁 Drop Capabilities and Use Read-Only Filesystems
Linux capabilities are fine-grained root privileges. Docker containers inherit more of them than they typically need. Drop everything, then add back only what the app requires:
services:
myapp:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # only if binding to ports <1024
read_only: true
tmpfs:
- /tmp
- /var/run
read_only: true prevents an attacker from writing scripts or modifying config inside the container. Pair it with tmpfs mounts for directories the app needs to write to at runtime. See the Linux capabilities reference for the full list.
This technique complements Docker hardened images, which strip capabilities and users at build time — a natural next step in any Docker security for self-hosting hardening plan.
6. 📊 Limit Container Resources
An unrestricted container can consume all available RAM or CPU — through a memory leak, a bug, or intentional cryptojacking. Hard limits prevent one container from taking down your entire homelab:
services:
myapp:
image: myapp:latest
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
memory: 128M
These limits work with standalone Compose (not just Swarm) since Docker 20.10+.
7. 🔄 Keep Images Updated — But Carefully
Outdated images are the most reliable Docker security vulnerability in self-hosting. Watchtower automates updates:
services:
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --schedule "0 3 * * *" --cleanup
⚠️ Critical: Watchtower pulls and restarts containers in-place with no rollback. For databases, password managers, and Immich — take a snapshot before enabling auto-updates, or use manual update runs: docker run --rm containrrr/watchtower --run-once.
🔁 CI/CD: Automate Your Security Gate
Even a simple shell script on a cron job can enforce image scanning before deployment. This makes Docker security for self-hosting systematic rather than dependent on memory:
#!/bin/bash
IMAGE="nginx:latest"
trivy image --exit-code 1 --severity HIGH,CRITICAL "$IMAGE"
if [ $? -ne 0 ]; then
echo "❌ Image scan failed. Critical CVEs found. Aborting."
exit 1
fi
echo "✅ Scan passed. Deploying."
docker compose up -d
Place this script in your deployment flow. For monitoring what’s actually running after deployment, Dozzle gives you real-time log visibility without exposing the Docker socket broadly. For GUI-based Docker management with some security features built in, Portainer is worth looking at — just be aware it requires Docker socket access.
❓ FAQ
Q: Does rootless Docker work inside a Proxmox LXC container?
Running rootless Docker inside a privileged LXC is possible but requires enabling user namespaces on the Proxmox host (echo 1 > /proc/sys/kernel/unprivileged_userns_clone). Unprivileged LXC is more constrained. Refer to the Proxmox Linux Container documentation for container feature flags.
Q: Is Podman more secure than Docker for self-hosting?
Podman is rootless by default and daemonless — a real security advantage for Docker security for self-hosting. If you’re already heavily invested in Docker Compose workflows, migration has a cost. But if you’re starting fresh, Podman and podman-compose are worth evaluating seriously.
Q: Should I ever mount the Docker socket into a container?
Only when unavoidable (Watchtower, Portainer, some reverse proxies). Mounting /var/run/docker.sock gives a container root-equivalent control over the host daemon. Use the Tecnativa Docker Socket Proxy to expose only the specific API endpoints each container needs, rather than the full socket.
Wrapping Up 🎯
Implementing Docker security for self-hosting doesn’t require enterprise tooling — it requires revisiting your docker-compose.yml files with the right checklist. Start with secrets management and network isolation today — those two changes alone eliminate the most common attack paths. Rootless mode and image scanning come next. Capability dropping and resource limits are the final layer that makes your setup resilient even against zero-days.
Your homelab is worth protecting. It takes one exposed service and one unpatched CVE to lose control of your LAN. Docker security for self-hosting, done right, makes that scenario much harder to pull off.
