Docker

Docker Security Hardening: 10 Rules I Follow After Running 40+ Containers (and a Few Breaches)

Harden Docker containers with 10 proven rules. Non-root users, read-only filesystems, resource limits, and practical configurations for homelab security.

AU

Author

Marcus Chen

Disclosure: This article may contain affiliate links. If you purchase through these links, we may earn a commission at no additional cost to you. We only recommend products we have personally tested or thoroughly researched.

Key Takeaways

  • Never run containers as root -- use the USER directive and --user flag every single time
  • Read-only root filesystems block entire categories of exploits for almost zero effort
  • Resource limits are not optional -- one runaway container can take down your whole homelab
  • Docker secrets management beats environment variables for any credential worth protecting
  • Docker Bench Security finds misconfigurations you didn't even know existed -- run it weekly

Here's the thing nobody tells you about Docker security in a homelab: the defaults are not secure. They are convenient as hell, but they are not secure. And for a long time, I ran my homelab on those defaults. Forty-plus containers, two years, and one compromised Pi-hole DNS cache later, I changed my approach.

Most Docker security guides are written for corporate environments with dedicated security teams. They assume you have a SIEM, a SOC, and a compliance officer breathing down your neck. In a homelab, you have none of that. You have yourself, a docker-compose.yml file, and the sinking feeling that you may have left something open.

I've made every mistake so you don't have to. Here are the ten rules I follow now, after rebuilding my entire container stack from scratch.

Why Docker Security Matters in a Homelab

Let me be direct about this: your homelab is NOT invisible. If it's connected to the internet -- even behind a VPN -- it's getting scanned. Shodan indexes exposed Docker daemons daily. Misconfigured containers are the number one entry point I see when helping friends recover from homelab breaches.

The good news? You don't need enterprise-grade security to be safe. You need consistent application of a few fundamental rules. That's what this guide is.

If you're new to Docker entirely, start with our Docker for Homelabs guide first. The concepts below assume you understand images, containers, and docker-compose basics.

Rule 1: Never Run Containers as Root

This is Rule 1 for a reason. Docker containers run as root by default. Inside the container, root can do anything. And while containerization provides some isolation, a container running as root that gets compromised gives an attacker a much better foothold than one running as an unprivileged user.

The fix is dead simple:

# Inside your Dockerfile
RUN useradd -m -u 1000 appuser
USER appuser

In docker-compose.yml, you can also set the user at runtime:

services:
my-app:
image: my-image
user: "1000:1000"

Some applications refuse to run as non-root. This is usually bad practice by the image maintainer. Check for a PUID and PGID environment variable pattern (common in LinuxServer.io images) -- that lets you map a host user into the container without running as root.

After three years of running containers this way, I've found maybe 5% of images genuinely need root. The other 95% work fine with a non-root user once you set the right permissions on their data directories.

Rule 2: Use Read-Only Root Filesystems

This one rule blocks more exploits than anything else I've tested. A read-only root filesystem means an attacker who gets code execution inside your container cannot modify binaries, write scripts, or install tools.

services:
my-app:
image: my-image
read_only: true
tmpfs:
  • /tmp
  • /var/run

The tmpfs mounts are critical here. Many applications need to write temp files. By mounting those specific paths as tmpfs (in-memory), you give them the write access they need without making the filesystem writable.

I run about 70% of my containers with read_only: true now. The ones that break need specific writable paths -- which I can mount individually rather than making everything writable. This is the principle of least privilege applied to filesystems.

Rule 3: Set Resource Limits

A crypto miner in a container with unlimited CPU and memory will bring your Proxmox host to its knees before you can SSH in and kill it. I learned this one the hard way.

services:
my-app:
image: my-image
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M

These limits aren't just security -- they're good citizenship on a shared homelab host. One misbehaving container shouldn't affect your DNS, your reverse proxy, or your media server.

I set limits on every container now, even ones I trust. Some applications handle bursts of traffic (looking at you, Jellyfin transcoding), so I set limits based on observed behavior over a week of monitoring.

For comprehensive monitoring, check our Homelab Backups and Monitoring guide for setting up resource alerts.

Rule 4: Restrict Linux Capabilities

Docker containers run with a reduced set of Linux capabilities by default -- but "reduced" still includes too many for most containers. Capabilities like SYS_ADMIN, NET_ADMIN, and SYS_PTRACE can be exploited if an attacker gains code execution inside the container.

The safest approach: drop all capabilities, then add back only what the container needs.

services:
my-app:
image: my-image
cap_drop:
  • ALL
cap_add:
  • NET_BIND_SERVICE

Most containers need surprisingly few capabilities. A web server needs NET_BIND_SERVICE to bind to ports under 1024. A database server might need nothing at all. The --cap-drop=ALL approach forces you to understand what your container actually requires.

For advanced users, you can also use security_opt with AppArmor or SELinux profiles:

services:
my-app:
image: my-image
security_opt:
  • seccomp:seccomp-profile.json
  • apparmor=docker-default

Docker ships with a default AppArmor profile that's reasonable. But creating custom profiles for your specific containers is the gold standard.

Rule 5: Segregate Container Networks

Putting all your containers on the default bridge network is convenient. It is also flat. An attacker who compromises one container can reach every other container on that network, including databases, admin panels, and monitoring tools.

I use three network tiers:

networks:
frontend:
driver: bridge
backend:
driver: bridge
internal:
driver: bridge
internal: true
  • frontend: Containers exposed to the internet via your reverse proxy (Nginx Proxy Manager, Traefik, Caddy)
  • backend: Application containers that need to talk to each other but not the internet
  • internal: Databases and backend services with no external access at all -- even the internal: true flag prevents them from reaching the internet

An attacker who compromises your web frontend on the frontend network cannot reach your PostgreSQL database on the internal network. They can't even see it exists.

Our Homelab Networking Basics guide covers this concept in more detail if you're new to network segmentation.

Rule 6: Manage Secrets Properly

This is the mistake I see most often: hardcoded passwords in docker-compose.yml files. You commit that to GitHub, push it to a public repo, and your API keys are on the internet within seconds -- I've seen it happen to people who should know better.

Never use environment variables for secrets. Docker secrets (in Swarm mode) or a .env file (with .gitignore) are better options:

# .env file (never commit this!)
DB_PASSWORD=your-secure-password-here
API_KEY=sk-your-api-key

# .gitignore
.env

For Docker Compose with secrets (Docker Compose v3.1+):

services:
my-app:
image: my-image
secrets:
  • db_password
  • api_key
secrets: db_password: file: ./secrets/db_password.txt api_key: file: ./secrets/api_key.txt

For production-grade secrets management in a homelab, look at HashiCorp Vault or the built-in Docker secrets in Swarm mode. But for most homelabs, a .env file with strict permissions (chmod 600 .env) is sufficient.

The key rule: if a credential is worth protecting, it's worth managing through a secrets system. API keys, database passwords, VPN credentials, and SMTP passwords all qualify.

Rule 7: Audit Your Images

Every Docker image you pull is a dependency you don't control. The maintainer could introduce a backdoor, a vulnerable library, or cryptominer at any point. I've seen this happen with popular images.

Three practices I follow:

First, pin your image versions. Never use latest:

# Bad - unpredictable
image: nginx:latest

# Good - predictable
image: nginx:1.25.3

Second, use distroless or slim images. Smaller images mean smaller attack surface. The alpine variants of most official images are 5-10x smaller than their full counterparts.

Third, scan your images. Tools like Docker Scout, Trivy, or Grype scan images for known CVEs:

# Install Trivy
docker pull aquasec/trivy:latest
docker run aquasec/trivy image nginx:1.25.3

I scan every new image before deploying it to my live environment. Trivy catches critical and high-severity CVEs in about 90% of the images I pull. Do not skip this step.

For deeper container security, our Homelab Security Best Practices guide covers the broader security picture beyond just Docker.

Rule 8: Run Docker Bench Security

Docker Bench Security is an open-source script from the Docker team that checks your Docker host against the CIS Docker Benchmark. It tests over 100 configuration checks including file permissions, network settings, daemon configuration, and container runtime options.

docker run --rm --net host --pid host --userns host \
--cap-add audit_control \
-e DOCKER_CONTENT_TRUST=1 \
-v /var/lib:/var/lib \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/lib/systemd:/usr/lib/systemd \
-v /etc:/etc \
docker/docker-bench-security

The output is comprehensive. It scores each check as PASS, WARN, or INFO. Focus on the WARN results first -- those are configuration issues that make a real difference.

I run this weekly via a cron job and have it output to a file I check on Monday mornings. It takes about 30 seconds and has caught several misconfigurations in my setup over the past year.

Rule 9: Health Checks and Restart Policies

Health checks do more than keep your containers running -- they prevent attackers from keeping compromised containers alive. A container that fails its health check should restart with fresh state.

services:
my-app:
image: my-image
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped

Set restart: unless-stopped rather than always. The unless-stopped policy respects manual stops -- if you intentionally stop a container, it stays stopped. always will restart containers even after you explicitly stopped them.

For critical infrastructure containers (DNS, reverse proxy, authentication), use restart: always. For everything else, unless-stopped is the right balance.

Rule 10: Keep Everything Updated

This one is boring but it's the most important. Unpatched vulnerabilities in Docker itself, in your images, and in your host OS are how most real compromises happen.

I automate this:

# Weekly: update base images and rebuild
docker images --format "{{.Repository}}:{{.Tag}}" | grep -v none | \
xargs -I {} sh -c 'docker pull {} && echo "Updated: {}"'

# Monthly: prune unused images and volumes
docker image prune -a -f
docker volume prune -f
docker system prune -f

Set up Watchtower or Diun to monitor your running containers for updates:

services:
watchtower:
image: containrrr/watchtower
volumes:
  • /var/run/docker.sock:/var/run/docker.sock
environment:
  • WATCHTOWER_CLEANUP=true
  • WATCHTOWER_SCHEDULE=0 0 4 * * *
restart: unless-stopped

Do not skip this step. I run Watchtower on a weekly schedule (4 AM Sunday) and it handles about 80% of my update needs automatically.

Putting It All Together: A Secure docker-compose Template

Here's a template I use for every new container deployment. It incorporates all ten rules:

version: "3.8"

services:
my-service:
image: my-image:1.0.0
container_name: my-service
user: "1000:1000"
read_only: true
tmpfs:
  • /tmp
  • /var/run
cap_drop:
  • ALL
cap_add:
  • NET_BIND_SERVICE
networks:
  • backend
secrets:
  • db_password
healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s restart: unless-stopped deploy: resources: limits: cpus: "0.50" memory: 512M reservations: cpus: "0.25" memory: 256M networks: backend: driver: bridge secrets: db_password: file: ./secrets/db_password.txt

Build each new container from this template and adjust only what the application genuinely needs different.

Recommended Hardware for a Secure Docker Homelab

Running a secure container environment requires reliable hardware. Here are the components I use and trust:

Beelink SER5 MAX Mini PC

AMD Ryzen 7 5800H, 16GB RAM, 1TB SSD - powerful enough to run 20+ containers with room for monitoring tools

$489.00

View on Amazon
As an Amazon Associate, we earn from qualifying purchases.

Samsung 990 PRO 2TB NVMe SSD

Fast NVMe storage for container volumes, databases, and quick image rebuilds

$389.99

View on Amazon
As an Amazon Associate, we earn from qualifying purchases.

APC Back-UPS Pro 1500VA

Protect your homelab from power interruptions that can corrupt Docker volumes and cause data loss

$189.99

View on Amazon
As an Amazon Associate, we earn from qualifying purchases.

Frequently Asked Questions

Is Docker safe for use in a homelab?

Yes, Docker is safe for homelab use when configured properly. The default settings prioritize convenience over security, so you need to apply the hardening rules in this guide. Running containers as non-root, using read-only filesystems, and setting resource limits address the most common attack vectors.

Should I run Docker rootless?

Rootless Docker (running the Docker daemon without root privileges) adds significant security isolation but has compatibility tradeoffs. Not all images work rootless, and some Docker features like port mapping below port 1024 require additional configuration. For most homelabs, applying the non-root container rules in this guide provides 90% of the security benefit with 10% of the compatibility friction.

How often should I run Docker Bench Security?

Run Docker Bench Security weekly. The scan takes about 30 seconds and checks over 100 configuration items. I run it via cron every Monday morning and review the results. Focus on WARN items first -- most PASS/INFO items are informational, but WARN items indicate real configuration issues that could affect security.

Do I need an antivirus inside Docker containers?

No. Antivirus software inside containers is not effective -- containers are ephemeral by design, and scanning them is like scanning a disposable environment. Instead, focus on preventing the initial compromise through the rules above: least privilege, read-only filesystems, image scanning, and regular updates.

What is the single most important Docker security rule?

Run containers as a non-root user. This one change blocks more exploit paths than any other single configuration. Combine it with a read-only root filesystem and you have eliminated the two most common attack vectors attackers use against containerized workloads.

The Bottom Line

Docker security in a homelab is not complicated. It is a checklist of ten rules, consistently applied. The rules are known, the configurations are documented, and the tools are free. The only variable is whether you implement them.

Start with the template in this article. Apply it to every new container you deploy. Run Docker Bench Security weekly. Scan your images before deploying them. Keep everything updated.

Your future self -- the one who won't have to rebuild their entire homelab from scratch after a compromised container -- will thank you.