What baseline controls look like for small teams
Here’s the short answer: a workable Docker baseline for small teams is a dozen controls you can ship in two weeks. Use minimal, pinned images; run as non-root with read-only filesystems; drop Linux capabilities and enforce seccomp; keep secrets out of images; scan and sign images in CI; restrict network paths; patch hosts fast; and log, alert, and verify. It’s enough to stop common container breakouts and supply-chain surprises.
Organize the effort by three gates: build (image hygiene), ship (registry and provenance), and run (runtime and network). Assign owners: Dev leads own Dockerfiles; DevOps owns CI, registry, and Kubernetes policy; IT/Sec owns host patching and alerting. Measure outcomes weekly: percentage of non-root containers, unsigned images blocked, and time to remediate critical CVEs in base images. Keep the scope tight, then iterate.
Build images you can trust: minimal, pinned, reproducible
Start with minimal bases (distroless, Ubuntu minimal, or Alpine with care). Pin everything by digest, not tag, and lock package versions. Use multi-stage builds to keep compilers and package managers out of the final image. Prefer static binaries where practical. For reproducibility, set explicit locales, UID/GID, and file timestamps during build, and ensure the final CMD/ENTRYPOINT is deterministic. Generate an SBOM to record exactly what shipped, including OS packages and app dependencies.
Scanning is not a checkbox. Establish a triage policy: fail builds on critical CVEs with known exploits; warn on medium findings and time-box remediation. Add image labels such as org.opencontainers.image.revision and build-date for traceability. Avoid :latest in Dockerfiles and deployments; use immutable digests. Remove shells and package managers from runtime stages to shrink the attack surface. If you need a shell for debugging, keep a separate debug image, never in production.
Keep secrets out of images and logs
Never bake API keys, database passwords, or tokens into images or Git. Inject at runtime: Docker Swarm secrets, Kubernetes Secrets with KMS envelope encryption, or an external vault. Prefer file-mounted secrets with restrictive permissions (0400) over environment variables, which often leak into crash dumps, metrics, or debugging output. Example: mount db/password at /var/run/secrets/db and have the app read the file on startup, refreshing from the source when rotated.
Prevent accidental disclosure upstream. Add .env, id_rsa, and credentials.json to .dockerignore so they never enter build contexts. Run secret scanning pre-commit and in CI to block pushes containing keys. Limit interactive shells in production; disable history and redact sensitive fields at the logger or reverse proxy. Review application telemetry and error reporting to ensure no secret values or authorization headers are ever echoed to logs, traces, or analytics dashboards.
Harden containers at runtime
Run as a non-root user with a dedicated UID/GID and minimal filesystem permissions. Drop all Linux capabilities, then add back only what the process genuinely needs; many web apps need none. Enforce seccomp (Docker’s default or a tuned profile), set read-only root filesystems, mount /tmp as tmpfs, and enable no-new-privileges. Block privilege escalation (allowPrivilegeEscalation: false) and avoid host mounts; if you must, use narrow, read-only bind mounts with node selectors and taints.
Apply CPU and memory limits to prevent noisy-neighbor issues and fork bombs. Set ulimits (nofile, nproc) to sane bounds and implement healthchecks that actually exercise downstream dependencies. Validate volume ownership and permissions at container start. Expect friction: an app may break after capabilities are dropped or FS is read-only. Reproduce locally, observe denials, then grant narrowly. For higher-risk workloads, consider gVisor or Kata Containers, trading a small performance hit for stronger isolation.
Guard the supply chain in CI/CD
Make provenance visible and enforceable. A practical pipeline: 1) pull base images by digest; 2) build with BuildKit for determinism; 3) run tests; 4) scan with Trivy or Grype and fail on criticals; 5) generate an SBOM (Syft); 6) sign images and attestations with Sigstore Cosign; 7) push to a private, immutable registry; 8) deploy using digests only, never tags. Store build metadata with the commit and release notes.
At deploy, verify what you built is what you run. Use admission policies (Kyverno or OPA Gatekeeper) to ban :latest, require non-root, enforce readOnlyRootFilesystem, and validate signatures before pods start. Enable registry tag immutability and retention rules to prevent drift. Avoid self-hosted CI runners on production nodes. Keep Cosign public keys or Fulcio roots in version control. Add a rollback plan that references prior signed digests and their SBOMs.
Kubernetes, networking, and the edge of trust
Adopt the Kubernetes Pod Security Standards at least baseline, ideally restricted. Set runAsNonRoot and a dedicated runAsUser; drop capabilities via securityContext; require seccompProfile: RuntimeDefault; and mount volumes read-only unless justified. Block hostPath and hostNetwork. Default-deny traffic with NetworkPolicies, then allow only what’s needed, e.g., app pods to talk to db on 5432 and outbound DNS. Keep liveness checks from bypassing policies by scoping them to cluster IPs.
Tighten ingress: terminate TLS with modern ciphers, enforce HSTS, and rate-limit. Consider mTLS for service-to-service paths via a service mesh where justified. Use imagePullSecrets bound to namespaces and least-privilege service accounts with minimal RBAC. Patch nodes and container runtimes quickly; kernel CVEs are container CVEs. Restrict egress at cloud firewalls to limit SSRF blast radius. Enable audit logs and centralize them; monitor for pods running as root or with host mounts.
A sprint-ready checklist
Checklist: pin images by digest; multi-stage builds; run as non-root; drop capabilities; enable seccomp; read-only rootfs with tmpfs /tmp; inject secrets at runtime via files; scan images and produce SBOMs in CI; sign images and verify at deploy; block :latest and require policies; default-deny network; set CPU/mem limits and ulimits; patch hosts quickly; centralize logs with basic alerts on root, host mounts, and unsigned images.
Prove it works: 1) count containers violating non-root or read-only policies; 2) measure percentage of images deployed by digest; 3) track time to remediate critical base image CVEs; 4) simulate a blocked deploy for an unsigned image; 5) run a capability drop test and verify the app still functions; 6) confirm egress blocks unexpected external endpoints. Publish these metrics to the team dashboard and review in retrospectives.
Common mistakes and practical next steps
Frequent pitfalls include using :latest tags, baking secrets into images, over-broad CAP_SYS_ADMIN grants, ignoring egress controls, postponing kernel patches, and enabling every scanner rule so alerts get silenced. Expect breakage when you flip to read-only filesystems or drop capabilities. Triage with short-lived canaries, log denials, and add the minimum required change. Document exceptions with an expiry date; re-review them alongside dependency updates.
Start with the highest-value five items: non-root, read-only rootfs, capability drops, CI scanning with SBOM, and signature verification at deploy. Aim to complete those in two weeks, then iterate on network policies and runtime isolation. If you need help tuning policies or integrating signing and attestations, Auranik Cloud Solutions & IT Services (/poland/technology/cloud) can support design and rollout. Always verify details against your cloud vendor and Kubernetes documentation before enforcing changes in production.
Community content reflects individual experiences and should not be treated as legal, immigration, financial or government advice.
Know someone who may find this guide useful?