E-NO Logo
EN FR
Docker container security 7 Min Read

Docker container security hardening for safer runtime defaults: practical implementation guide

calendar_today Published: 2026-07-08
update Last Updated: 2026-07-08
analytics SEO Efficiency: 97%
Technical guide illustration for Docker container security hardening for safer runtime defaults: practical implementation guide.

Default containers often run as root, can write everywhere, and hold more kernel powers than most apps need. That increases risk if an app is compromised. This guide shows how to set safer Docker defaults with practical snippets you can apply in minutes: non-root users, minimal Linux capabilities, read-only filesystems with tmpfs for writable paths, and keeping sensible seccomp and AppArmor defaults. You will get Dockerfile, docker run, and Compose examples, plus a small local pilot plan and common pitfalls.

Workflow Overview

  1. Build images that run as a non-root user by default. Fix file ownership at build time.
  2. Prefer high ports in the container and map from the host.
  3. Drop all Linux capabilities, then add back only what is required.
  4. Make the container filesystem read-only and provide tmpfs for runtime write needs like /tmp.
  5. Keep Docker's default seccomp profile and the AppArmor docker-default where available.
  6. Add runtime limits like no-new-privileges, pids-limit, and resource caps.
  7. Test locally with a narrow, measurable pilot before expanding.

Dockerfile: non-root and ownership

Create a dedicated user and group, fix ownership during build, and run as that user. Example for a generic web service that listens on 8080:

# syntax=docker/dockerfile:1
FROM debian:12-slim

# Install only what you need (example runtime deps)
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates \
 && rm -rf /var/lib/apt/lists/*

# Create app user and directories
RUN groupadd --system app && useradd --system --create-home --gid app app
WORKDIR /app

# Copy app files and set ownership
COPY --chown=app: app . /app

# Expose a high port to avoid extra capabilities
EXPOSE 8080

# Drop root at build time
USER app: app

# Use a simple entrypoint
CMD ["./server"]

Notes:

  • Use --chown during COPY so files are writable by the app user where needed.
  • Prefer high ports like 8080 inside the container. Map host port 80 to 8080 when you run it, avoiding NET_BIND_SERVICE.
  • Multi-stage builds keep images smaller and reduce attack surface.

Capabilities and networking

Containers inherit a default set of Linux capabilities. Drop them by default and add back only what your app really needs.

Safer pattern when you do not need privileged networking:

docker run --rm \
  --cap-drop ALL \
  --security-opt no-new-privileges: true \
  -p 80:8080 myapp: latest

If you must bind to a low port (for example 80) inside the container, either:

  • Prefer host mapping: -p 80:8080 and keep CAP_NET_BIND_SERVICE out.
  • Or add only the specific capability:
docker run --rm \
  --cap-drop ALL --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges: true \
  -p 80:80 myapp: latest

Avoid privileged containers and avoid adding broad capabilities like SYS_ADMIN unless there is a clear, minimal, and audited reason.

Read-only filesystems and tmpfs

A read-only root filesystem blocks many write-based attacks and reduces accidental damage. Combine it with tmpfs for writable paths your app needs at runtime.

Runtime example:

docker run --rm \
  --read-only \
  --tmpfs /tmp: rw, noexec, nosuid, nodev, size=64m \
  --tmpfs /var/run: rw, noexec, nosuid, nodev, size=16m \
  -p 80:8080 myapp: latest

Tips:

  • Point your app logs to stdout/stderr instead of files. Docker will capture logs.
  • If your app must persist data, mount an explicit volume or bind mount to a single path, with read-write as needed and read-only elsewhere.
  • Use noexec, nosuid, nodev on tmpfs where possible.

Seccomp and AppArmor defaults

Docker applies a default seccomp profile that blocks many risky syscalls. Keep this default unless you have a known need to relax it. Avoid unconfined.

  • Seccomp: The default is applied automatically. Do not set --security-opt seccomp=unconfined. Only provide a custom seccomp profile if you have a specific case.
  • AppArmor: On AppArmor-enabled hosts, the docker-default profile is applied. Keep it, or bind a stricter profile with --security-opt apparmor=docker-default (or a custom profile name) when available.

Compose example that keeps defaults and adds no-new-privileges:

services:
  web:
    image: myapp: latest
    ports:
      - "80:8080"
    security_opt:
      - "no-new-privileges: true"
      - "apparmor: docker-default"  # if AppArmor is enabled on the host

If you supply a custom seccomp profile file, reference it explicitly:

docker run --rm \
  --security-opt seccomp=./seccomp-profile.json \
  myapp: latest

Runtime and Compose examples

Putting it together with several safer defaults:

docker run example:

docker run --rm \
  --user 1000:1000 \
  --cap-drop ALL \
  --security-opt no-new-privileges: true \
  --read-only \
  --tmpfs /tmp: rw, noexec, nosuid, nodev, size=64m \
  --pids-limit 200 \
  --cpus 1 --memory 256m \
  -p 80:8080 myapp: latest

Equivalent Compose snippet:

services:
  web:
    image: myapp: latest
    user: "1000:1000"
    ports:
      - "80:8080"
    read_only: true
    tmpfs:
      - "/tmp: rw, noexec, nosuid, nodev, size=64m"
    cap_drop:
      - ALL
    security_opt:
      - "no-new-privileges: true"
      - "apparmor: docker-default"
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 256M

Note: On non-AppArmor hosts, remove the AppArmor line. Resource limits under deploy require a supported orchestrator; for local Compose you can also use mem_limit and cpus in compose v2 syntax.

Local Pilot Plan

Start narrow and measurable so you can inspect outcomes locally before rollout.

Goal: Run one service as non-root with a read-only filesystem, tmpfs for /tmp, dropped capabilities, and no-new-privileges. Verify the app still serves traffic and fails safely when trying to write to disallowed paths.

Plan:

  1. Choose one simple service (static site or basic API).
  2. Update its Dockerfile to add a dedicated user and set USER.
  3. Run with --cap-drop ALL, --read-only, tmpfs for /tmp, and no-new-privileges.
  4. Map host port 80 to container 8080 to avoid extra capabilities.
  5. Verify:
  • Non-root: docker exec -it <cid> id and confirm uid != 0.
  • Filesystem: docker exec <cid> sh -c 'touch /etc/blocked' should fail.
  • Writable tmp: docker exec <cid> sh -c 'touch /tmp/ok' should succeed.
  • Traffic: curl localhost:80 should return a 200.
  1. Document any missing writes and provide a specific volume or tmpfs path for them.

Success criteria:

  • The app serves traffic.
  • Writes to non-tmp paths fail as expected.
  • No added capabilities beyond what is required.

This narrow pilot is fast to inspect and sets a baseline for the next services.

Troubleshooting and production pitfalls

Common issues and fixes:

  • Permission denied on startup: Files copied as root are not writable by the app user. Fix with COPY --chown=app: app and check directory ownership.
  • App cannot write logs: Send logs to stdout/stderr or mount a dedicated writable directory for logs.
  • Package managers fail at runtime: With --read-only there is no write access for apt or apk. Install everything at build time and keep runtime lean.
  • Binding to port 80 fails: Use -p 80:8080 and keep the container on 8080. If you must bind to 80 inside, add only NET_BIND_SERVICE.
  • Temporary files break: Provide tmpfs for /tmp and any runtime socket directories like /var/run.
  • Custom seccomp or AppArmor needed: Only relax what is necessary. Start from defaults and add minimal allowances.
  • Inconsistent behavior across hosts: AppArmor may not be present on all hosts. Keep configuration conditional and test on target OS.

Image scanning boundaries:

  • Scanning helps find known CVEs in images, but it does not ensure safe runtime settings. Combine scanning with the hardening steps here.
  • Runtime misconfigurations like running as root or leaving broad capabilities are outside the scope of image scanners.

Operational tips:

  • Pin base image digests to reduce surprise changes.
  • Use multi-stage builds to keep images small.
  • Add healthchecks and resource limits to stop runaway processes.

Conclusion

Safer Docker defaults are straightforward: run as a non-root user, drop capabilities by default, keep the filesystem read-only with tmpfs for required writes, and rely on Docker's default seccomp and AppArmor profiles unless you have a proven need to customize. Apply no-new-privileges and add resource limits. Start with a narrow local pilot, verify behavior, then roll out to more services. These steps reduce risk while keeping your operations simple and inspectable.

Article Quality Score

Reader usefulness 97%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL