Security
Narrow the surface
Containers share the host kernel. Isolation comes from namespaces, resource limits from cgroups, and filtering from capabilities, seccomp, and AppArmor/SELinux. Hardening is a stack of small defaults, not one switch.
Practical posture: run as a non-root USER in the Dockerfile, use --read-only with --tmpfs /tmp, apply --cap-drop ALL --cap-add NET_BIND_SERVICE (add back only what you can name), set --security-opt no-new-privileges, and avoid --privileged, which disables nearly every control at once.
Secrets: docker secret create plus --secret in swarm mounts a file under /run/secrets/<name> - it is not an environment variable unless you read it into one. Build-time secrets use RUN --mount=type=secret, never ARG, which leaks through image history.
Daemon side: /var/run/docker.sock access is root-equivalent, so membership in the docker group is a privilege grant. Enable TLS with client certificates for remote API access on 2376 and never expose plain 2375. Authorization plugins and rootless mode reduce impact. Content trust (DOCKER_CONTENT_TRUST=1) enforces signed images on pull and push.
Image hygiene: minimal bases, no credentials baked into layers, regular scanning, digests pinned.
Traps
A USER instruction is defeated by an entrypoint script that starts as root and never drops privileges with su/gosu. Adding a user without chown-ing the workdir causes confusing permission errors. Secrets passed as ENV or ARG show up in docker history and docker inspect.
Rule of thumb
Drop all capabilities, then add back the single one you can justify by name.