Containers

Container Security: How to Harden Your Docker Images

Harden Docker containers with minimal base images, non-root users, dropped capabilities, read-only filesystems, CVE scanning in CI, and Kubernetes Pod Security Standards.

A
Abhishek Patel8 min read

Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

Container Security: How to Harden Your Docker Images
Container Security: How to Harden Your Docker Images

Your Container Is Only as Secure as Its Weakest Layer

Container security isn't a single tool or practice -- it's a layered defense that starts with your base image choice and extends through build pipelines, runtime configuration, and orchestrator policies. Most teams get the basics wrong: running as root, shipping full OS images with hundreds of known CVEs, and mounting the Docker socket into containers like it's no big deal.

I've audited containerized environments where a single compromised container led to full cluster takeover because nobody bothered to drop Linux capabilities or set a read-only root filesystem. These aren't exotic attacks -- they're the first things any penetration tester tries.

What Is Container Security?

Definition: Container security encompasses the practices, tools, and configurations used to protect containerized applications throughout their lifecycle -- from image building and registry storage to runtime execution and orchestration. It covers image hardening, vulnerability scanning, access control, network segmentation, and kernel-level isolation.

Hardening Your Docker Images: Step by Step

  1. Start with a minimal base image -- Alpine (7 MB), distroless (2 MB), or scratch (0 MB) instead of ubuntu or debian
  2. Pin your base image digest -- use FROM alpine:3.19@sha256:abc123... instead of floating tags
  3. Create a non-root user and switch to it with USER
  4. Drop all Linux capabilities and add back only what you need
  5. Set the filesystem to read-only and mount writable tmpfs where needed
  6. Scan for CVEs in CI before the image reaches any registry
  7. Never store secrets in the image -- use build-time secrets or runtime injection

Minimal Base Images: Your First Line of Defense

Every package in your base image is a potential attack vector. A standard Ubuntu image has hundreds of packages, most of which your application never uses. Each one can have vulnerabilities.

Base ImageSizeTypical CVEsShellPackage Manager
ubuntu:22.0477 MB50-100+bashapt
debian:bookworm-slim74 MB30-80bashapt
alpine:3.197 MB0-5ashapk
gcr.io/distroless/static2 MB0-2NoneNone
scratch0 MB0NoneNone

Pro tip: Distroless images from Google are purpose-built for security. They contain only your application and its runtime dependencies -- no shell, no package manager, no curl. If an attacker gets code execution, they can't easily drop to a shell or download tools.

Running as Non-Root

By default, containers run as root (UID 0). This is the single most common security misconfiguration. If an attacker exploits a vulnerability in your app, they have root access inside the container -- and with certain misconfigurations, that can mean root on the host.

FROM node:20-alpine

# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm ci --production

# Switch to non-root before CMD
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]

Verifying Non-Root at Runtime

# Check which user the container is running as
docker exec my-container whoami
# Should NOT output "root"

# Check the process UID
docker exec my-container id
# uid=1000(appuser) gid=1000(appgroup)

Dropping Linux Capabilities

Linux capabilities split root's power into granular permissions. By default, Docker grants containers a subset of capabilities, but it's still more than most apps need. The secure approach: drop all capabilities and add back only the ones your application requires.

# Drop all capabilities, add back only NET_BIND_SERVICE
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE my-app:latest
# In Kubernetes Pod spec
securityContext:
  capabilities:
    drop:
      - ALL
    add:
      - NET_BIND_SERVICE
  runAsNonRoot: true
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false

Watch out: Never run containers with --privileged in production. This flag gives the container ALL capabilities, access to all host devices, and disables most security mechanisms. A privileged container can trivially escape to the host.

Read-Only Root Filesystem

A read-only root filesystem prevents attackers from writing malicious binaries, scripts, or configuration changes to the container's filesystem. Most applications only need to write to specific directories like /tmp or a data directory.

# Docker: read-only root with writable /tmp
docker run --read-only --tmpfs /tmp:rw,noexec,nosuid my-app:latest
# Kubernetes: read-only root with emptyDir for /tmp
containers:
- name: app
  image: my-app:latest
  securityContext:
    readOnlyRootFilesystem: true
  volumeMounts:
  - name: tmp
    mountPath: /tmp
volumes:
- name: tmp
  emptyDir: {}

CVE Scanning in CI

Every image you build should be scanned for known vulnerabilities before it reaches a registry. The goal is to catch CVEs early -- in the pull request, not in production.

Popular Scanning Tools Compared

ToolTypeCostCI IntegrationDatabase
TrivyOSSFreeGitHub Actions, GitLab CIMultiple (NVD, Alpine, etc.)
GrypeOSSFreeGitHub ActionsAnchore feed
Snyk ContainerSaaSFree tier / $25+/dev/moAll major CISnyk Vulnerability DB
Docker ScoutSaaSIncluded with Docker subDocker CLI nativeMultiple sources
AWS ECR ScanningCloudFree (basic) / Inspector pricingNative to ECRClair / Inspector
# GitHub Actions: Scan with Trivy
- name: Scan image for vulnerabilities
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: my-app:latest
    format: table
    exit-code: 1
    severity: CRITICAL,HIGH

Pro tip: Set exit-code: 1 in your scanner config to fail the CI pipeline when critical vulnerabilities are found. A scan that only reports but never blocks is just security theater.

Secrets Management

Secrets in Docker images are a common and serious mistake. Once a secret is in an image layer, anyone who pulls the image can extract it -- even if you delete the file in a later layer.

What NOT to Do

# WRONG: Secret baked into the image
ENV DATABASE_URL=postgres://admin:password@db:5432/mydb
COPY .env /app/.env

What to Do Instead

  • Build-time secrets (Docker BuildKit): RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
  • Runtime environment variables: Pass via docker run -e or Kubernetes Secrets
  • External secret stores: HashiCorp Vault, AWS Secrets Manager ($0.40/secret/month), or SOPS for encrypted files in Git

Kernel-Level Security: seccomp and AppArmor

Beyond container configuration, Linux kernel security modules provide additional isolation:

seccomp Profiles

seccomp restricts which system calls a container can make. Docker ships a default profile that blocks about 44 syscalls, but you can create custom profiles for tighter control.

# Run with Docker's default seccomp profile (enabled by default)
docker run --security-opt seccomp=default my-app:latest

# Run with a custom profile
docker run --security-opt seccomp=custom-profile.json my-app:latest

AppArmor Profiles

AppArmor provides mandatory access control, restricting what files and capabilities a process can access. Docker applies a default AppArmor profile to all containers on systems where AppArmor is available.

Kubernetes Pod Security Standards

Kubernetes v1.25+ replaced PodSecurityPolicy with Pod Security Standards (PSS) enforced via the Pod Security Admission controller. Three built-in levels:

LevelWhat It AllowsUse Case
PrivilegedNo restrictionsSystem-level workloads (CNI, storage drivers)
BaselineBlocks known privilege escalationsMost application workloads
RestrictedHardened, follows current best practicesSecurity-sensitive workloads
# Enforce restricted policy on a namespace
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/audit: restricted

Pricing: Security Tools and Services

ToolFree TierPaid Pricing
TrivyFully free (OSS)N/A
Snyk Container200 tests/monthFrom $25/dev/month (Team)
HashiCorp VaultOSS self-hostedHCP Vault from $0.03/hr
AWS Secrets ManagerNone$0.40/secret/month + $0.05/10K API calls
Sysdig SecureNoneCustom pricing (runtime security)

Frequently Asked Questions

Is running as root inside a container dangerous if the container is isolated?

Yes. Container isolation isn't perfect -- kernel vulnerabilities, misconfigured volume mounts, or the Docker socket can allow container escapes. Running as root inside the container means root on the host if isolation breaks. Non-root is defense in depth: even if isolation fails, the attacker has limited privileges.

Do I need to scan images if I'm using Alpine or distroless?

Yes, always. Alpine has fewer CVEs than Ubuntu, but it still has them. Your application dependencies (npm packages, Python libraries, Go modules) also have vulnerabilities that base-image choice doesn't affect. Scan everything, every build.

What's the difference between seccomp and AppArmor?

seccomp filters system calls -- it controls what operations a process can ask the kernel to do. AppArmor controls file and resource access -- what paths a process can read, write, or execute. They complement each other. Docker enables default profiles for both on supported systems.

Should I use Kubernetes Pod Security Standards or a third-party policy engine?

Start with Pod Security Standards -- they're built in and cover the most important security baselines. If you need more granular control (custom policies, mutation, audit logging), add OPA Gatekeeper or Kyverno. Most teams don't need a policy engine until they have multiple teams deploying to the same cluster.

How do I handle secrets in Kubernetes?

Kubernetes Secrets are base64-encoded, not encrypted, by default. For production, enable encryption at rest in etcd, or better yet, use an external secret store (Vault, AWS Secrets Manager) with the External Secrets Operator to sync secrets into the cluster. Never commit secrets to Git in plain text.

Can Docker Scout replace Trivy or Snyk?

Docker Scout is a solid choice if you're already paying for Docker Desktop subscriptions. It integrates natively with docker build and provides remediation advice. However, Trivy is free, faster in CI, and supports more artifact types. For most teams, Trivy is the pragmatic default.

Conclusion

Container security isn't optional, and it's not something you bolt on after deployment. Use minimal base images, run as non-root, drop capabilities, set read-only filesystems, scan in CI, and manage secrets externally. These six practices eliminate the vast majority of container-related vulnerabilities. Start with the Dockerfile changes -- they take five minutes and make the biggest difference.

A

Written by

Abhishek Patel

Infrastructure engineer with 10+ years building production systems on AWS, GCP, and bare metal. Writes practical guides on cloud architecture, containers, networking, and Linux for developers who want to understand how things actually work under the hood.

Related Articles

Enjoyed this article?

Get more like this in your inbox. No spam, unsubscribe anytime.

Comments

Loading comments...

Leave a comment

Stay in the loop

New articles delivered to your inbox. No spam.