We Scanned 16 Popular Docker Compose Projects — Here's What We Found

We ran dockguard against the official docker-compose.yml files for 16 of the most-recommended self-hosted projects on r/selfhosted. The scan turned up 252 findings across 25 services. Every project had at least one critical issue. Authentik alone had 15 criticals.

This post walks through the numbers, the five issues that show up everywhere, and the one-line fixes for each.

TL;DR

Metric Count
Projects scanned 16
Total services 25
Critical findings 49
Warnings 155
Info 48
Total findings 252
Avg per project 15.8
Projects with at least one critical 16 / 16 (100%)

The projects: Authentik, Audiobookshelf, Gitea, Grafana, Home Assistant, Jellyfin, Linkding, Mealie, n8n, Nextcloud, Paperless-ngx, Pi-hole, Portainer, Traefik, Uptime Kuma, Vaultwarden. Two more (Immich, wg-easy) failed to parse — dockguard parser bugs, on us to fix.

Methodology

Each project's compose file was pulled from its official GitHub repo or, where the repo didn't ship one, the recommended quick-start example from its documentation. We did not modify the files. The scan was run with dockguard scan on the unedited YAML.

dockguard is a single Go binary — no Docker daemon required to scan, no SaaS roundtrip, no telemetry. Findings are categorised as critical, warning, or info based on what an attacker or runaway process could do given the configuration as-shipped.

Per-project breakdown

Project Services Critical Warning Info Total
Authentik 4 15 25 8 48
Paperless-ngx 3 2 19 6 27
Gitea 2 4 13 4 21
n8n 2 4 13 4 21
Nextcloud 2 4 13 4 21
Traefik 2 2 13 2 17
Pi-hole 1 4 6 2 12
Grafana 1 2 6 2 10
Mealie 1 2 6 2 10
Portainer 1 2 6 2 10
Vaultwarden 1 2 6 2 10
Audiobookshelf 1 1 6 2 9
Home Assistant 1 2 5 2 9
Jellyfin 1 1 6 2 9
Linkding 1 1 6 2 9
Uptime Kuma 1 1 6 2 9

Authentik is the outlier — it's also the most complex of the bunch (auth server, worker, database, redis), so the surface is larger. Even the simplest single-service apps (Uptime Kuma, Linkding) average 9 findings.

The five issues that show up in almost everything

Issue Occurrences Severity
Plaintext secrets in env vars 26 Critical
Missing security_opt: no-new-privileges 25 Warning
No memory limit 25 Warning
No CPU limit 25 Warning
Running as root 25 Warning
Writable root filesystem 25 Warning
Full Linux capabilities 24 Warning
Ports exposed to 0.0.0.0 22 Critical
No healthcheck defined 25 Info
Restart without healthcheck 23 Info

Five patterns explain the bulk of the report. Each has a fix that's usually one line of YAML.

1. Plaintext secrets in compose files

26 occurrences. Critical. Database passwords, admin tokens, encryption keys — all sitting in plain text in the file most users commit to git. Vaultwarden's quick-start ships with ADMIN_TOKEN as plaintext. Authentik defaults PG_PASS and AUTHENTIK_SECRET_KEY to literal strings. n8n's example pins DB_POSTGRESDB_PASSWORD inline.

Fix: pull secrets from a .env file (gitignored) or use Docker secrets.

services:
  app:
    image: vaultwarden/server
    environment:
      ADMIN_TOKEN: ${ADMIN_TOKEN}   # read from .env, not committed

2. Every project exposes ports to all interfaces

22 occurrences. Critical. ports: "8080:80" binds to 0.0.0.0:8080 — every network interface on the host. Most self-hosters run behind a reverse proxy on the same machine, so the container only needs to listen on localhost.

Fix: one prefix.

ports:
  - "127.0.0.1:8080:80"

This single change closes the most common accidental-exposure path on a home server with a public IP.

3. Nobody drops Linux capabilities

24 of 25 services. By default, a container gets a broad set of Linux capabilities — CAP_CHOWN, CAP_NET_RAW, CAP_SYS_CHROOT, and a dozen more. A web app reading and writing files needs none of them. An attacker who pops a shell in the container inherits the lot.

Fix: drop everything, add only what you need.

services:
  app:
    cap_drop:
      - ALL
    # cap_add: [NET_BIND_SERVICE]   # only if binding to a low port

For most web apps the cap_add block stays empty.

4. Zero compose files set resource limits

25 occurrences each for memory and CPU. A single misbehaving container can consume all host RAM or pin every core. On a shared homelab box that takes everything down with it.

Fix:

services:
  app:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"

(Outside Swarm mode, mem_limit and cpus at the service level also work.)

5. No healthchecks, but restart: always

25 services with no healthcheck. 23 of those set restart: always or restart: unless-stopped. Without a healthcheck, "running" means the container's PID 1 is alive — not that the app inside actually serves requests. A container can wedge in a broken state and restart back into the same wedged state indefinitely.

Fix: define a healthcheck and let restart react to it.

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

What this means

None of these projects are insecure software. Authentik, Gitea, Nextcloud, Vaultwarden — these are mature, well-audited codebases. The issue is that the example compose files they ship are optimised for "works on first try" rather than "safe in production." Users copy the example, run docker compose up -d, and inherit defaults nobody actually intended for an internet-facing host.

The fixes are short. They are also tedious to apply manually across 20-service stacks, which is the gap dockguard fills:

$ go install github.com/narrowcastdev/dockguard@latest
$ dockguard scan docker-compose.yml

The scanner is local-only — your compose file never leaves the machine. Source, install one-liner, and the full ruleset are at github.com/narrowcastdev/dockguard.

Methodology caveats

If you maintain one of the projects above and want the full per-finding report, the scanner output is reproducible from the public compose files — or open an issue on dockguard and we'll send it.