Docker Container Security: User, Capabilities, Read-Only Filesystem and Seccomp
AI generated
Docker Security · Container Hardening · Linux Capabilities · Seccomp
Container Security:
User, Capabilities, Read-Only Filesystem and Seccomp

A container that runs as root, with every Linux capability and a writable filesystem, is technically speaking a privileged process behind a thin namespace curtain. Docker offers several complementary security layers: a non-root USER, minimal capabilities, a read-only filesystem, and seccomp profiles. This article shows how these layers work and how to implement them in real Dockerfiles and Compose configurations.

15 min read Non-root · Capabilities · Read-Only-FS · Seccomp · AppArmor · Docker Bench Docker 25+ · Linux Kernel 5.x+ · CIS Docker Benchmark

1. Understanding the container threat model

Misunderstandings about container security often start with a flawed mental model: containers are not virtual machines. They share the Linux kernel with the host and with every other container on the same host. A vulnerability in the kernel namespace system, a flawed syscall implementation, or a kernel exploit can let an attacking process inside a container escalate into the host context. The question is not whether containers provide isolation, they do. The question is how strong that isolation is and against which attack vectors it actually holds.

The realistic attack scenario for web containers looks like this: an application has a remote code execution vulnerability (RCE). An attacker manages to run arbitrary code inside the container. At that point, the container security configuration decides what that code can do: can it write to the filesystem? Can it open network connections? Can it execute privileged syscalls? Can it reach other containers on the same host? Every security layer, non-root, read-only filesystem, capability drop, seccomp, answers one of these questions with "no" and thereby limits the blast radius of a successful attack.

Defense in depth is the principle behind the different container security layers: no single measure is absolutely secure, but several layers stacked together raise the effort for an attacker exponentially. A non-root container with a read-only filesystem, minimized capabilities, and a seccomp profile is significantly harder to abuse than a root container with all the defaults. This article covers each layer individually, explains its protection mechanism, and shows how to implement it in production Dockerfiles and Compose configurations.

2. Non-root USER: the single most important measure

By default, Docker containers run as root (UID 0). That means a process inside the container has the same privileges as root on the host, unless other restrictions apply. For many attack paths, escapes via namespace vulnerabilities, mountpoint exploits, having root inside the container is a prerequisite. The simplest and most effective container security measure is therefore running the container as an unprivileged user: the USER directive in the Dockerfile sets the user for all subsequent RUN, CMD, and ENTRYPOINT instructions.

Putting this into practice takes care: files and directories the application needs to access must be owned by the application user. Ports below 1024 can only be bound by root, so containers should listen on ports above 1024 (typically 8080 instead of 80) while an external load balancer or reverse proxy handles the port forwarding. The USER concept is complemented by user namespace remapping at the daemon level: with user namespace remapping enabled, root inside the container is mapped to an unprivileged host UID, adding another protective ring against container escape exploits.


# Dockerfile: Non-root user setup for PHP-FPM application
FROM php:8.4-fpm-alpine

# Create application user with specific UID/GID
# Use fixed IDs for predictable ownership in volume mounts
RUN addgroup -g 1001 -S appgroup && \
    adduser  -u 1001 -S appuser -G appgroup

WORKDIR /var/www/html

# Install dependencies as root (they modify system dirs)
RUN --mount=type=cache,id=apk-cache,target=/var/cache/apk \
    apk add --no-cache libzip libpng

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Copy application, owned by appuser from the start
COPY --chown=appuser:appgroup composer.json composer.lock ./

# Install dependencies as root, but with COMPOSER_CACHE_DIR owned by appuser
RUN --mount=type=cache,id=composer,uid=1001,gid=1001,target=/composer-cache \
    COMPOSER_CACHE_DIR=/composer-cache composer install \
      --no-dev --no-interaction --prefer-dist --no-scripts --optimize-autoloader

COPY --chown=appuser:appgroup . .

# PHP-FPM config: listen as appuser
RUN sed -i 's/www-data/appuser/g' /usr/local/etc/php-fpm.d/www.conf

# Switch to non-root user: all subsequent operations run as appuser
USER appuser

# Health check runs as appuser
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD php-fpm -t || exit 1

EXPOSE 9000
CMD ["php-fpm"]

3. Linux capabilities: minimal privileges for containers

Linux splits root privileges into granular units: Linux capabilities. By default, Docker grants containers a defined subset of these capabilities, including CHOWN, DAC_OVERRIDE, SETUID, NET_BIND_SERVICE, and others. Each of these capabilities is a potential foothold for privilege escalation. The principle of least privilege recommends dropping all capabilities (--cap-drop=ALL) and adding back only the ones explicitly needed (--cap-add=NET_BIND_SERVICE). Most web applications need no capabilities at all beyond NET_BIND_SERVICE (and only if binding to a port below 1024).

In practice, the hardest question is: which capabilities does my application actually need? This can be worked out step by step with docker run --cap-drop=ALL --cap-add=... , adding capabilities one at a time until the application works. A faster route is docker run --security-opt=no-new-privileges:true: this flag prevents processes inside the container from acquiring new privileges via SUID binaries or syscalls, even if capabilities are still present. no-new-privileges is a sensible addition to --cap-drop and guards against a class of privilege escalation attacks that cap-drop alone does not cover.

4. Read-only filesystem: protection against runtime modification

The read-only filesystem (--read-only) prevents processes inside the container from modifying the container image's filesystem. This is a strong defense against a common attack technique: after an initial RCE vulnerability, attackers write malware, web shells, or backdoors to the filesystem, which then execute on the next request or container start. With --read-only, no write access is possible, so these persistence mechanisms simply do not work.

The practical problem: many applications need to write to temporary directories, PHP session files, log files, temporary upload files. The solution is selective tmpfs mounts on exactly the directories that need to be writable. A tmpfs mount is a RAM-backed filesystem that disappears when the container stops, so it offers no persistence foothold for malware. With --tmpfs /tmp:noexec,nosuid,nodev,size=256m, /tmp becomes writable, but executables placed there cannot be run (noexec). Typical tmpfs mounts for PHP applications: /tmp, /var/lib/php/sessions, /run, and application-specific cache directories.


# docker-compose.yml: Full security hardening for a PHP web container
services:
  web:
    image: registry.example.com/app:${APP_VERSION}
    user: "1001:1001"
    read_only: true

    # Grant write access only to specific directories via tmpfs (in-memory, no exec)
    tmpfs:
      - /tmp:mode=1777,noexec,nosuid,nodev,size=128m
      - /var/lib/php/sessions:mode=0700,noexec,nosuid,size=64m
      - /var/run:noexec,nosuid,size=16m

    # Drop ALL capabilities, add back only what is strictly needed
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE   # Only needed if binding port < 1024 (prefer 8080!)

    security_opt:
      - no-new-privileges:true
      - seccomp:./seccomp/app-profile.json    # Custom seccomp profile
      - apparmor:docker-app-profile            # AppArmor profile (if available)

    # Prevent container from running as privileged
    privileged: false

    # Limit resources to reduce DoS blast radius
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 512M
        reservations:
          memory: 128M

    # Health check verifies actual application health
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

5. Seccomp profiles: syscall filtering for containers

Seccomp (Secure Computing Mode) is a Linux kernel feature that restricts a process's syscalls to an allowed whitelist. By default, Docker applies a seccomp profile that already blocks around 44 dangerous syscalls, including ptrace, perf_event_open, and several namespace syscalls that can be used for container escape exploits. For maximum hardening, an application-specific seccomp profile can be created that only allows the syscalls actually needed.

Building a minimal seccomp profile for an application is laborious but doable: using strace -f -e trace=all ... 2>&1 | grep syscall or seccomp's audit mode, you can observe which syscalls an application actually uses under real conditions. That list then becomes the seccomp profile's whitelist. Docker's default profile is a good starting point; the difference lies in specifically blocking syscalls a PHP application never needs (mount, swapon, reboot, all namespace syscalls). The seccomp profile format is JSON and is applied via --security-opt seccomp=profile.json.

6. AppArmor and LSM: mandatory access control

AppArmor is a Linux Security Module (LSM) that implements mandatory access control (MAC) on a path basis. An AppArmor profile for a container defines which file paths the container may read from and write to, which network operations are allowed, and which capabilities can be used. On Ubuntu and Debian-based hosts, Docker applies the docker-default profile by default, which provides solid baseline protection.

For critical containers, an application-specific AppArmor profile makes sense. It is installed on the host (apparmor_parser -r -W /etc/apparmor.d/docker-app-profile) and assigned to the container via --security-opt apparmor=docker-app-profile. The combination of non-root user, capability drop, read-only filesystem, seccomp, and AppArmor forms a defense-in-depth architecture in which each layer covers a different attack vector, so the failure of a single layer does not lead to full compromise.

7. Security configuration in Docker Compose

Docker Compose translates security configuration directly into Docker run parameters. The key fields: user for the container user, read_only: true for the read-only filesystem, cap_drop and cap_add for capabilities, security_opt for seccomp profiles and AppArmor, privileged: false (set it explicitly to document the default) and tmpfs for writable in-memory directories. These settings apply per service and can be defined in a base Compose file as a template that other services pull in via extends.

A common mistake: privileged: true gets set for debugging purposes and is then forgotten. In production, privileged: true effectively disables every container security layer, the container gets full access to the host. An automated lint step in the CI pipeline that checks Compose files for privileged: true prevents this kind of oversight. docker compose config prints the resolved configuration and shows which security parameters are actually active, including defaults. It is a valuable debugging tool for container security audits.

8. Security audit: Docker Bench and Trivy

Docker Bench for Security is an open-source script that checks Docker installations and container configurations against the CIS Docker Benchmark. It checks host configuration, Docker daemon settings, image security, and container runtime configuration. Running Docker Bench automatically in the CI pipeline after the build identifies configuration issues before an image reaches production. The output is structured: PASS, WARN, and INFO entries with references to the CIS Benchmark and concrete recommendations.

Trivy complements Docker Bench with vulnerability scanning: it checks the image against known CVEs in base image packages, application dependencies (composer.lock, package-lock.json), and configuration files. Trivy can be integrated directly into CI pipelines and fails the build on critical CVEs before the image is deployed. The combination of static image scanning (Trivy) and configuration auditing (Docker Bench) covers the most important container security aspects automatically, turning security into a step in the normal deployment pipeline rather than a manual review.


# ci-security-check.sh: Automated container security validation in CI pipeline
set -euo pipefail

IMAGE="${1:?Image name required}"
FAIL_ON_SEVERITY="${2:-CRITICAL}"

echo "=== Trivy: Vulnerability scan ==="
trivy image \
  --exit-code 1 \
  --severity "${FAIL_ON_SEVERITY}" \
  --no-progress \
  --format table \
  "${IMAGE}"

echo "=== Trivy: Dockerfile misconfiguration check ==="
trivy config \
  --exit-code 1 \
  --severity HIGH,CRITICAL \
  --no-progress \
  Dockerfile

echo "=== Non-root user check ==="
USER=$(docker inspect "${IMAGE}" --format '{{.Config.User}}')
if [ -z "$USER" ] || [ "$USER" = "root" ] || [ "$USER" = "0" ]; then
  echo "[FAIL] Container runs as root, set USER in Dockerfile"
  exit 1
fi
echo "[PASS] Container user: ${USER}"

echo "=== Docker Bench (subset): image checks ==="
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  docker/docker-bench-security \
  -c container_images 2>/dev/null \
  | grep -E "^\[WARN\]|^\[PASS\]|^\[FAIL\]"

echo "=== All security checks passed ==="

9. Security layers compared

The different container security measures address different attack vectors and carry different implementation effort. No single measure is sufficient on its own.

Security layer Attack vector Effort Impact
Non-root USER Privilege escalation, kernel exploits Low High
Read-only filesystem Malware persistence, web shells Low (+ tmpfs) High
Capability drop Privilege escalation via syscalls Medium (testing required) High
no-new-privileges SUID exploits, privilege escalation Minimal Medium
Seccomp profile Kernel exploits via syscalls High (build a profile) Very high

For most production containers, the practical recommended stack is: non-root USER (always), read-only filesystem with tmpfs for write directories (almost always), --cap-drop=ALL with minimal --cap-add (for every new service), no-new-privileges (always, at no cost), Docker's default seccomp profile (active automatically, verify manually). Custom seccomp profiles pay off for especially critical services. The security stack is not a one-time effort, it has to be part of the normal Dockerfile review and the CI pipeline.

Mironsoft

Container security, hardening audits, and security pipeline integration

Ready to systematically harden your container infrastructure?

We analyze your container configurations against the CIS Benchmark, implement non-root, read-only filesystem, capability drop, and seccomp profiles, and integrate Trivy and Docker Bench into your CI/CD pipeline.

Security audit

Reviewing Dockerfile and Compose configurations against the CIS Docker Benchmark

Hardening implementation

Non-root, read-only filesystem, capability drop, and seccomp for production containers

CI security pipeline

Integrating Trivy scanning and Docker Bench checks automatically into every build pipeline

10. Summary

Container security is not a single measure but a defense-in-depth stack. A non-root USER stops an attacking process from having root privileges. A read-only filesystem with tmpfs for write directories prevents malware persistence. Capability drop with --cap-drop=ALL and minimal --cap-add restricts privileged operations. no-new-privileges blocks SUID-based privilege escalation. Docker's default seccomp profile blocks dangerous kernel syscalls. Together, these layers drastically reduce the blast radius of a successful attack.

The most important practical takeaway: most of these measures carry no performance cost and create no development friction. Non-root, no-new-privileges, and the default seccomp profile are effectively free. Read-only filesystem with tmpfs requires a few lines of configuration. Capability drop requires testing. The total effort for a solidly hardened container setup is one to two days of implementation work, and the security improvement is permanent, paying off on every deployment cycle that follows.

Container Security: The essentials at a glance

Non-root USER

USER 1001:1001 in the Dockerfile. Pass all application files with --chown. Use ports above 1024, no NET_BIND_SERVICE needed.

Read-only filesystem

--read-only plus tmpfs for /tmp and /run with noexec,nosuid. Prevents malware persistence after RCE exploits.

Capabilities

--cap-drop=ALL, then only explicitly needed --cap-add. Always set no-new-privileges:true, it blocks SUID exploits.

Seccomp and audit

Keep Docker's default seccomp active or use a custom profile for critical services. Integrate Trivy plus Docker Bench into the CI pipeline.

11. FAQ: Docker Container Security

1Why is root inside a container a risk?
Containers share the kernel with the host. Root processes have more room for privilege escalation onto the host in the event of kernel exploits.
2Why reduce Linux capabilities?
--cap-drop=ALL minimizes footholds for privilege escalation. Return only explicitly needed capabilities via --cap-add.
3--read-only and the application writes to /tmp?
Combine it with a tmpfs mount on /tmp:noexec,nosuid, a RAM-backed filesystem that is writable, offers no persistence attack vector, and disappears on stop.
4What does no-new-privileges do?
Prevents new privileges via SUID binaries or execve(). Protects against a privilege escalation class even when capabilities are still present.
5Seccomp vs. AppArmor?
Seccomp: syscall filtering (kernel level). AppArmor: path-based access control (files, network). They complement each other against different attack vectors.
6Which capabilities does my app need?
Start with --cap-drop=ALL and watch for errors. Or use strace/seccomp's audit mode for detailed syscall logging.
7What does Docker Bench for Security check?
Host configuration, Docker daemon, images, and container runtime against the CIS Docker Benchmark. PASS/WARN/FAIL with concrete recommendations.
8What does Trivy scan?
CVEs in OS packages, application dependencies (composer.lock, package-lock.json), and Dockerfile misconfigurations. CI integration fails the build on critical CVEs.
9What is privileged: true and why is it dangerous?
Disables every security restriction: seccomp, AppArmor, capabilities. Effectively root access to the host. Never use it for application containers.
10Performance overhead of security hardening?
Practically none for non-root, no-new-privileges, seccomp, and capability drop. The effort lies only in initial implementation and testing.