docker-container-security
Skill16 starsChanged 6 months agoMITView on GitHub
Run containers with a defensive baseline that survives production. Covers non-root users, read-only filesystems, dropped Linux capabilities, secret mounts instead of build-time bake-in, image scanning with trivy, distroless and minimal base images, and the Docker-bypasses-UFW firewall pitfall. Invoke when adding Docker to a VPS with UFW, writing a new Dockerfile, or pushing an image to a public registry.
- Reads credentials
In this repository37 other files it gives its agents.
- agent-client-securitySkill
- ai-agent-guardrailsSkill
- android-securitySkill
- api-securitySkill
- auth-hardeningSkill
- backend-architectureSkill
- backup-disaster-recoverySkill
- cloudflare-hardeningSkill
- codebase-auditSkill
- dach-complianceSkill
- dependency-supply-chainSkill
- distributed-system-auditSkill
- dns-domain-securitySkill
- email-deliverability-securitySkill
- file-upload-securitySkill
- gdpr-technical-controlsSkill
- github-actions-securitySkill
- honeypot-tarpitsSkill
- incident-responseSkill
- ios-securitySkill
- kubernetes-securitySkill
- llm-app-securitySkill
- llm-coding-failure-modesSkill
- log-strategySkill
- mcp-securitySkill
- message-bus-securitySkill
- nextjs-securitySkill
- object-storage-securitySkill
- payload-cms-securitySkill
- postgres-hardeningSkill
- prompt-injection-defenseSkill
- rag-securitySkill
- secret-hygieneSkill
- site-server-auditSkill
- stripe-webhook-securitySkill
- vps-hardeningSkill
- wordpress-hardeningSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 4 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 16
After a base-image CVE that affects your stack
- Distinctive: line 56
Every production Dockerfile should:
- Distinctive: line 168
Run a scanner on every image before deploy.
- Distinctive: line 191
Default Docker bridge network puts all containers on one flat L2 segment.
- Very specific: line 236
Before deploying any docker-compose.yml to production, walk this:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Docker / Container Security
A pragmatic baseline for Docker on a single VPS or a small cluster. Covers the Dockerfile, the run-time configuration, and the host-side gotchas — particularly the UFW-bypass that catches most people once.
When to invoke
- Installing Docker on a VPS that has UFW (read the UFW section first — Docker bypasses UFW by default)
- Writing a new Dockerfile or
docker-compose.ymlfor production - Pushing an image to a public registry
- Periodic audit of running containers
- After a base-image CVE that affects your stack
The UFW bypass — read this first
Docker manipulates iptables directly. By default, ports published with -p are exposed to the world, even if UFW says they should not be. ufw status will mislead you.
Two options:
Option A — use ufw-docker (community-maintained, robust):
# Install ufw-docker
sudo wget -O /usr/local/bin/ufw-docker \
https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
sudo ufw-docker install
sudo systemctl restart ufw
# Then allow per-container:
sudo ufw-docker allow web 80/tcpOption B — bind to localhost when you front with a reverse proxy:
# docker-compose.yml — bind to 127.0.0.1, not 0.0.0.0
services:
app:
ports:
- "127.0.0.1:9000:9000" # host nginx proxies to thisVerify:
sudo ss -tlnp | grep docker # should not show 0.0.0.0:<port> for internal servicesDockerfile baseline
Every production Dockerfile should:
- Run as a non-root user
- Pin the base image to a specific digest or version tag (not
latest) - Drop build tools from the final image
- Not contain secrets baked in
# Pin to specific Node version; consider digest pinning for stricter supply chain
FROM node:20.11.1-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:20.11.1-bookworm-slim
WORKDIR /app
# Create non-root user
RUN groupadd -r app && useradd -r -g app -d /app -s /sbin/nologin app
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=app:app . .
USER app
EXPOSE 3000
# Use exec form so the process becomes PID 1 and receives signals
CMD ["node", "server.js"]Notes:
-slimis much smaller than the default tag. Distroless (gcr.io/distroless/nodejs20-debian12) is smaller and shell-less — harder to live in if an attacker gets RCE- Multi-stage to leave compilers / dev deps out of the runtime image
--chownonCOPYso files are not owned by root inside the imageUSERlast so all later instructions run as that user
Run-time hardening
For each container, prefer the most restrictive run-time options the app actually tolerates.
docker run -d \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \ # only if the app needs to bind <1024
--security-opt no-new-privileges:true \
--pids-limit 200 \
--memory 512m \
--cpus 1 \
-p 127.0.0.1:3000:3000 \
--name myapp myapp:1.2.3In docker-compose.yml:
services:
app:
image: myapp:1.2.3
read_only: true
tmpfs:
- /tmp:size=64m,noexec,nosuid
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE]
security_opt:
- no-new-privileges:true
pids_limit: 200
mem_limit: 512m
cpus: 1.0
user: "1000:1000"
restart: unless-stoppedWhat each flag does:
--read-only— root filesystem cannot be modified. Writable dirs are explicittmpfsmounts or volumes.--cap-drop=ALL+ selective--cap-add— drop all Linux capabilities, add only what the app needs. Most apps need none.--security-opt no-new-privileges— process cannot gain privileges viasetuidbinaries.--pids-limit— caps fork-bomb DoS.--memory/--cpus— prevents one runaway container from killing the host.
Secrets — never bake them in
Anything in ENV or ARG ends up in image layers. Anyone who can pull the image can extract them.
# Bad
docker build --build-arg STRIPE_KEY=sk_live_... .
# Good — at runtime, from env
docker run -e STRIPE_KEY="$STRIPE_KEY" myapp
# Better — Docker secrets (Swarm) or mounted files
docker run -v /run/secrets/stripe:/run/secrets/stripe:ro myapp
# app reads /run/secrets/stripe
# Best — a real secret manager (Vault, cloud KMS) with short-lived tokensTo inspect a built image for accidental secrets:
docker history --no-trunc myapp:1.2.3
docker save myapp:1.2.3 -o myapp.tar
tar -xf myapp.tar
# search the layer tarballs for .env / known token prefixesImage scanning
Run a scanner on every image before deploy. Trivy is the most common choice and is fast enough for CI.
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:1.2.3In GitHub Actions:
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@<pinned-sha>
with:
image-ref: 'myapp:${{ github.sha }}'
severity: 'HIGH,CRITICAL'
exit-code: '1'Scan on a schedule too — vulnerabilities are discovered after images are built. A weekly re-scan catches newly-known CVEs in already-deployed images.
Network segmentation
Default Docker bridge network puts all containers on one flat L2 segment. They can reach each other freely.
# docker-compose.yml — isolate concerns
networks:
frontend:
backend:
internal: true # no external connectivity
database:
internal: true
services:
web:
networks: [frontend, backend]
api:
networks: [backend, database]
db:
networks: [database]internal: true networks have no NAT to the outside — only useful for intra-cluster communication. The DB cannot connect out to the internet, which limits exfil if compromised.
Periodic audit
# What is running with --privileged or as root?
docker ps --format 'table {{.Names}}\t{{.Image}}' | tail -n +2 | while read name img; do
user=$(docker inspect --format '{{.Config.User}}' "$name")
priv=$(docker inspect --format '{{.HostConfig.Privileged}}' "$name")
echo "$name image=$img user='${user:-<root>}' privileged=$priv"
done
# Containers with publicly-bound ports
docker ps --format '{{.Names}}\t{{.Ports}}' | grep -E '0\.0\.0\.0|:::' || echo "OK - none"
# Images older than 90 days
docker images --format '{{.Repository}}:{{.Tag}} {{.CreatedSince}}' | grep -E '(months|year)'
# Stopped containers + dangling images — clean up unless you need forensic state
docker container prune -f --filter 'until=720h' # 30 days
docker image prune -fCompose-file checklist
Before deploying any docker-compose.yml to production, walk this:
- No
latesttags; versions are pinned (and ideally digest-pinned for critical images) - Every service has
user:set to a non-root UID:GID read_only: truewherever the app tolerates it (most do)cap_drop: [ALL]+ minimalcap_addsecurity_opt: [no-new-privileges:true]mem_limitandpids_limitset- Ports bound to
127.0.0.1unless intentionally public - No secrets in
environment:— useenv_file:(gitignored), Docker secrets, or a manager - Internal networks (
internal: true) used to keep DB/cache off the public bridge restart: unless-stoppedrather thanalways(so manual stops stick)- Volumes are named, not anonymous (named volumes survive
docker compose down, anonymous do not)
What this skill will not do
- Help bypass container isolation on systems you do not own
- Recommend
--privileged,--cap-add=SYS_ADMIN, or running as root unless there is a documented, narrow need - Replace a full Kubernetes security review (different stack, different controls)
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

