Docker Container Security Hardening: Rootless Mode, Seccomp, AppArmor & Read-Only Roots
Learn how to secure Docker containers using Rootless Mode, Linux Seccomp, AppArmor, and Read-Only Filesystems with practical examples and production-ready configurations.
Introduction
Docker has transformed how applications are developed, packaged, deployed, and operated. Containers provide portability, consistency, and isolation, making them an essential part of modern application infrastructure.
However, running an application inside a Docker container does not automatically make it secure.
A poorly configured container can expose the host system to serious security risks. Running containers as root, granting unnecessary Linux capabilities, allowing unrestricted system calls, and providing unrestricted filesystem access can create opportunities for privilege escalation and container breakout attacks.
Consider a production environment where multiple microservices run on the same Kubernetes worker node. If one container becomes compromised and has excessive privileges, an attacker may attempt to access sensitive resources, manipulate the host, or compromise other workloads.
This is why Docker container security hardening is an essential part of production infrastructure design.
In this article, we will explore four important Linux security mechanisms:
- Rootless Docker Mode
- Seccomp (Secure Computing Mode)
- AppArmor (Application Armor)
- Read-Only Root Filesystems
We will also combine these mechanisms into practical Docker configurations and discuss how to apply them in production environments.
If you are exploring containerization, Linux security, and DevOps engineering, visit DevCampus for more technical tutorials and practical engineering guides.
1. Understanding Docker Container Security
Before discussing individual security mechanisms, let's understand how Docker containers achieve isolation.
Docker relies on several Linux kernel features:
- Namespaces: Isolate process IDs, networking, users, mounts, and other system resources.
- Control Groups (cgroups): Control CPU, memory, and other resource consumption.
- Linux Capabilities: Divide traditional root privileges into smaller permissions.
- Seccomp: Restrict the system calls a process can execute.
- AppArmor / SELinux: Enforce mandatory access control policies.
- User Namespaces: Map container users to different user IDs on the host.
These mechanisms work together to provide container isolation.
However, isolation is not equivalent to complete security.
For example, consider this command:
docker run -it ubuntu bash
The container may be isolated from other processes, but the application inside it could still run as root within its container namespace.
If additional privileges or dangerous host mounts are granted, the potential impact of a compromise increases.
A secure container should follow one fundamental principle:
Give every container only the permissions and resources it actually needs.
This is known as the Principle of Least Privilege.
2. Rootless Docker Mode
What Is Rootless Docker?
By default, the Docker daemon generally runs with root privileges.
This means the daemon has significant control over the host system, including the ability to create containers, manage filesystem mounts, and configure networking.
Rootless Docker changes this security model.
In Rootless Mode:
- The Docker daemon runs as a non-root user.
- Containers also run without requiring root privileges on the host.
- User namespaces provide UID and GID mapping.
- The impact of certain daemon and container vulnerabilities can be reduced.
The important distinction is that Rootless Mode protects both the Docker daemon and containers from relying on host-level root privileges.
How Rootless Mode Works
Consider the following example:
A normal Docker installation:
Host Operating System
|
| Root Privileges
|
Docker Daemon
|
Docker Container
|
Application
Rootless Docker:
Host Operating System
|
Normal Linux User
|
Rootless Docker Daemon
|
User Namespace
|
Docker Container
|
Application
The container's root user is mapped to an unprivileged user identity outside its user namespace.
This reduces the potential damage if an attacker gains control of a process inside the container.
Installing Rootless Docker
On a supported Ubuntu or Debian-based system, first install the required packages:
sudo apt update
sudo apt install -y uidmap dbus-user-session
For Docker Engine installations using the official Docker packages, install the rootless extras if they are not already available:
sudo apt install -y docker-ce-rootless-extras
Check whether the required UID mapping tools are available:
which newuidmap
which newgidmap
Check subordinate UID and GID ranges:
grep "^$(whoami):" /etc/subuid
grep "^$(whoami):" /etc/subgid
A user generally needs at least 65,536 subordinate UIDs and GIDs.
Install Rootless Docker:
dockerd-rootless-setuptool.sh install
Start the user-level Docker service:
systemctl --user start docker
Enable it at startup:
systemctl --user enable docker
Set the Docker socket:
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
Verify the configuration:
docker info
Look for:
Security Options:
seccomp
rootless
cgroupns
The presence of rootless indicates that the Docker client is connected to a rootless daemon.
Important: Rootless Docker has compatibility and operational limitations. For example, networking, storage drivers, privileged ports, and cgroup resource controls may behave differently depending on the kernel and system configuration.
Evaluate these requirements before adopting Rootless Mode in production.
3. Seccomp: Restricting Linux System Calls
What Is Seccomp?
Seccomp stands for Secure Computing Mode.
It is a Linux kernel security mechanism that restricts which system calls a process can execute.
Applications running inside containers interact with the Linux kernel through system calls.
Examples include:
open()read()write()socket()execve()mount()clone()
Most applications do not require access to every available Linux system call.
Allowing unnecessary system calls increases the kernel attack surface.
Seccomp helps reduce that attack surface by restricting system calls that a container can execute.
How Seccomp Works
Consider an application running inside a Docker container.
Without system-call restrictions:
Application
|
|
Linux System Calls
|
|
Linux Kernel
With Seccomp:
Application
|
|
System Call Request
|
|
Seccomp Filter
|
|---- Allowed System Call
|
|---- Blocked System Call
|
Linux Kernel
If an application attempts to execute a restricted system call, the kernel applies the configured Seccomp action.
Depending on the policy, the operation may fail with a permission error or trigger another configured action.
Docker's Default Seccomp Profile
Docker provides a default Seccomp profile.
It blocks a collection of potentially dangerous system calls while allowing the majority of system calls required by normal applications.
You should generally keep this default profile enabled instead of disabling it.
To explicitly use the default profile:
docker run -d \
--name secure-app \
--security-opt seccomp=builtin \
nginx:latest
The default profile provides a practical security baseline for many workloads.
Creating a Custom Seccomp Profile
Some applications require additional restrictions.
You can create a custom JSON profile.
Example: seccomp-profile.json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64"
],
"syscalls": [
{
"names": [
"read",
"write",
"openat",
"close",
"stat",
"fstat",
"mmap",
"mprotect",
"munmap",
"brk",
"rt_sigaction",
"rt_sigprocmask",
"ioctl",
"access",
"execve",
"exit",
"exit_group",
"arch_prctl",
"set_tid_address",
"set_robust_list",
"prlimit64",
"getrandom",
"futex"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
This is an illustrative allowlist, not a production-ready universal profile. Real applications need additional system calls depending on their runtime, architecture, libraries, and workload.
Run the container using the custom profile:
docker run -d \
--name secure-nginx \
--security-opt seccomp=./seccomp-profile.json \
nginx:latest
Seccomp Best Practices
- Never disable Seccomp without a strong technical justification.
- Start with Docker's default profile.
- Create workload-specific profiles only when required.
- Test custom profiles against application functionality.
- Monitor denied system calls.
- Avoid copying restrictive profiles between unrelated applications without testing.
For most production workloads, Docker's default Seccomp profile is a good starting point.
4. AppArmor: Mandatory Access Control for Containers
What Is AppArmor?
AppArmor stands for Application Armor.
It is a Linux Security Module (LSM) that provides mandatory access control.
While Seccomp restricts system calls, AppArmor controls what resources an application can access and how it can interact with them.
For example, an AppArmor policy can restrict an application from:
- Reading sensitive host files.
- Modifying protected system directories.
- Accessing specific filesystem paths.
- Performing certain privileged operations.
- Interacting with resources outside its permitted boundaries.
AppArmor applies security policies to processes rather than relying only on traditional Linux file permissions.
How AppArmor Works
Imagine a web application running inside a container.
The application may need access to:
/app
/etc/app
/var/log/app
However, it does not need access to:
/etc/shadow
/root
/proc/kcore
An AppArmor policy can restrict access to resources that the application does not require.
Conceptually:
Docker Container
|
|
Application Process
|
|
AppArmor Security Policy
|
|---- Allowed File Access
|
|---- Denied File Access
|
Linux Kernel
Checking AppArmor Status
On Ubuntu, check whether AppArmor is enabled:
sudo aa-status
Check whether the AppArmor service is running:
sudo systemctl status apparmor
Docker normally applies its built-in docker-default AppArmor profile to containers when AppArmor is available and enabled.
You can explicitly specify it:
docker run -d \
--name secure-web \
--security-opt apparmor=docker-default \
nginx:latest
Creating a Custom AppArmor Profile
For applications with specific security requirements, you can define custom AppArmor policies.
Create a profile:
sudo nano /etc/apparmor.d/docker-secure-app
Example:
#include <tunables/global>
profile docker-secure-app flags=(attach_disconnected) {
#include <abstractions/base>
file,
deny /etc/shadow r,
deny /root/** rwklx,
/app/** r,
/var/log/app/** rw,
deny mount,
deny ptrace,
}
This is a simplified illustrative policy. Production AppArmor profiles should be generated and validated against actual application behavior, including required executable access, libraries, networking, and writable directories.
Load the profile:
sudo apparmor_parser -r \
/etc/apparmor.d/docker-secure-app
Run a container with the custom profile:
docker run -d \
--name secure-app \
--security-opt apparmor=docker-secure-app \
nginx:latest
AppArmor Best Practices
- Keep AppArmor enabled on supported Linux hosts.
- Use the default Docker profile as a baseline.
- Create custom policies for applications with stricter requirements.
- Test policies before production deployment.
- Review denied operations using system audit logs.
- Avoid using
unconfinedunless there is a justified requirement.
Note: Rootless Docker has platform and feature limitations, including AppArmor support limitations in the documented Rootless implementation. Do not assume that every security mechanism is available in every Docker operating mode.
5. Read-Only Root Filesystem
What Is a Read-Only Root Filesystem?
A container's root filesystem contains its application binaries, libraries, configuration files, and other operating-system resources.
By default, a running container may be able to modify files inside this filesystem.
This creates a security risk.
For example, if an attacker compromises a web application, they may attempt to:
- Modify application binaries.
- Install malicious scripts.
- Replace executable files.
- Create persistence mechanisms.
- Modify configuration files.
A read-only root filesystem reduces these possibilities.
Docker provides the --read-only option to mount the container's root filesystem as read-only.
Running a Container with a Read-Only Filesystem
Example:
docker run -d \
--name secure-nginx \
--read-only \
nginx:latest
Now the container cannot modify its root filesystem.
However, there is one important consideration.
Many applications need writable directories for temporary files, logs, sockets, or runtime data.
A read-only filesystem may cause these applications to fail if writable paths are not configured.
Using tmpfs for Temporary Files
Docker supports temporary in-memory filesystems using --tmpfs.
For example:
docker run -d \
--name secure-nginx \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /var/cache/nginx:rw,noexec,nosuid,size=64m \
nginx:latest
Here:
-
--read-onlyprotects the root filesystem. -
/tmpprovides temporary writable storage. -
/var/cache/nginxprovides writable cache storage. -
noexecprevents execution of binaries from those mounts. -
nosuidprevents set-user-ID and set-group-ID bits from taking effect. -
size=64mlimits the temporary filesystem size.
The writable paths must be selected according to the actual application's requirements.
Using Read-Only Volumes
Sometimes an application needs to read configuration files but should not modify them.
You can mount a volume as read-only:
docker run -d \
--name secure-app \
--mount type=bind,source=/opt/app/config,target=/app/config,readonly \
nginx:latest
This prevents the container from modifying the mounted configuration directory through that mount.
Remember that read-only root filesystems and read-only volume mounts solve different problems. Both may be needed.
6. Additional Docker Security Hardening Techniques
Rootless Mode, Seccomp, AppArmor, and read-only filesystems are important, but production security requires additional controls.
6.1 Run Containers as Non-Root Users
Avoid running application processes as root inside containers whenever possible.
Example Dockerfile:
FROM nginx:stable
USER 10001
For a custom application image:
FROM ubuntu:24.04
RUN groupadd -g 10001 appgroup && \
useradd -u 10001 -g appgroup -m appuser
WORKDIR /app
COPY --chown=appuser:appgroup . /app
USER appuser
CMD ["./application"]
Ensure that the application has permission to access the directories and resources it requires.
6.2 Drop Unnecessary Linux Capabilities
Linux capabilities divide traditional root privileges into smaller permissions.
By default, Docker grants containers a limited set of capabilities.
However, applications may not need all of them.
You can drop all capabilities and add back only the required ones:
docker run -d \
--name secure-app \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
nginx:latest
Here, the container receives only the capability to bind privileged network ports, subject to the relevant namespace and runtime configuration.
Do not add capabilities automatically. Determine which ones the application actually requires.
6.3 Disable Privilege Escalation
Use:
docker run -d \
--name secure-app \
--security-opt no-new-privileges:true \
nginx:latest
This prevents processes from gaining additional privileges through mechanisms such as setuid and setgid executables.
6.4 Limit CPU and Memory
A compromised application can also consume excessive host resources.
Use resource limits:
docker run -d \
--name secure-app \
--memory=512m \
--cpus=1.0 \
--pids-limit=100 \
nginx:latest
This helps protect the host against resource exhaustion.
6.5 Avoid Privileged Containers
Avoid:
docker run --privileged ubuntu
Privileged containers receive extensive permissions and significantly weaken container isolation.
Use specific capabilities, device access, and security options instead of enabling privileged mode.
6.6 Scan Container Images
Container images may contain outdated packages, vulnerable dependencies, and unnecessary tools.
Scan images before deployment.
For example, using Trivy:
trivy image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
nginx:latest
Integrate vulnerability scanning into CI/CD pipelines so that vulnerable images can be identified before reaching production.
7. Production-Ready Docker Security Configuration
Let's combine multiple security controls into one example.
The following command demonstrates a hardened container configuration:
docker run -d \
--name production-app \
--read-only \
--user 10001:10001 \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--security-opt seccomp=builtin \
--security-opt apparmor=docker-default \
--memory=512m \
--cpus=1.0 \
--pids-limit=100 \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
-p 8080:8080 \
my-application:1.0
This example combines:
| Security Control | Purpose |
|---|---|
--read-only |
Prevents modifications to the root filesystem |
--user |
Runs the application as a non-root user |
--cap-drop ALL |
Removes unnecessary Linux capabilities |
no-new-privileges |
Prevents privilege escalation |
seccomp=builtin |
Uses Docker's built-in Seccomp profile |
apparmor=docker-default |
Applies Docker's default AppArmor profile |
--memory |
Limits memory consumption |
--cpus |
Limits CPU consumption |
--pids-limit |
Limits process creation |
--tmpfs |
Provides controlled temporary writable storage |
Production consideration: This is a security-hardening template, not a universal command that will work unchanged for every application. The image must support the specified non-root UID, port, writable paths, and runtime behavior. AppArmor also requires a compatible host configuration.
For an additional security layer, evaluate Rootless Docker and user namespace isolation.
8. Docker Compose Security Configuration
In real-world environments, applications are frequently deployed using Docker Compose.
You can apply many of the same security controls through Compose.
Example compose.yaml:
services:
application:
image: my-application:1.0
container_name: secure-application
user: "10001:10001"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
- seccomp=builtin
- apparmor=docker-default
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
mem_limit: 512m
cpus: 1.0
pids_limit: 100
ports:
- "8080:8080"
restart: unless-stopped
Deploy:
docker compose up -d
Inspect the running container:
docker inspect secure-application
Check its security configuration and runtime settings.
Note that Compose resource-limit support and behavior can depend on the Compose implementation and deployment environment.
9. Docker Security Hardening in Kubernetes
Docker security principles also apply to Kubernetes workloads.
Kubernetes provides container security settings through the Pod and container securityContext.
For example:
apiVersion: v1
kind: Pod
metadata:
name: secure-application
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: application
image: my-application:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
memory: "512Mi"
cpu: "1"
requests:
memory: "128Mi"
cpu: "100m"
This configuration applies several important security controls:
- Runs the application as a non-root user.
- Uses the runtime's default Seccomp profile.
- Prevents privilege escalation.
- Makes the root filesystem read-only.
- Drops unnecessary Linux capabilities.
- Defines CPU and memory limits.
For Kubernetes environments, AppArmor can also be configured using supported security profiles and runtime integrations.
Modern Kubernetes deployments should also evaluate Pod Security Admission and the Restricted Pod Security Standard.
Important: Kubernetes does not automatically make a workload secure just because it runs inside a container. Security settings must be deliberately configured and validated.
10. Common Docker Security Mistakes
Even experienced developers sometimes introduce security weaknesses through convenience-based configurations.
Here are some mistakes to avoid.
| Mistake | Security Risk | Recommended Approach |
|---|---|---|
| Running everything as root | Increases privilege-escalation impact | Use non-root users |
Using --privileged
|
Weakens container isolation | Grant only required capabilities |
| Disabling Seccomp | Expands kernel attack surface | Keep the default profile |
| Using AppArmor unconfined | Removes an additional security layer | Use an appropriate enforced profile |
| Writable root filesystem | Allows unwanted filesystem modifications | Use --read-only
|
| Mounting host directories unnecessarily | Can expose sensitive host resources | Use restricted mounts |
| No resource limits | Can cause resource exhaustion | Set CPU, memory, and PID limits |
| Using outdated images | Exposes known vulnerabilities | Scan and update images |
| Exposing Docker socket | Can grant extensive host control | Restrict access to the Docker API |
One especially important warning:
Never mount /var/run/docker.sock into an application container unless there is a well-understood and carefully controlled requirement.
Access to the Docker socket can provide extensive control over the Docker daemon and, depending on the host configuration, the underlying system.
11. Docker Security Hardening Checklist
Before deploying a container to production, review the following checklist.
Container Identity
- [ ] Run containers as non-root users.
- [ ] Evaluate Rootless Docker where appropriate.
- [ ] Avoid unnecessary root privileges.
Kernel Security
- [ ] Keep Seccomp enabled.
- [ ] Use the default Seccomp profile as a baseline.
- [ ] Enable AppArmor or an appropriate alternative LSM.
- [ ] Avoid unnecessary Linux capabilities.
- [ ] Prevent privilege escalation.
Filesystem Security
- [ ] Enable read-only root filesystems where possible.
- [ ] Use tmpfs for temporary writable data.
- [ ] Mount configuration volumes as read-only.
- [ ] Avoid unnecessary host filesystem mounts.
Runtime Security
- [ ] Set CPU and memory limits.
- [ ] Set process limits.
- [ ] Avoid privileged containers.
- [ ] Restrict network exposure.
- [ ] Monitor container activity.
Image Security
- [ ] Use trusted base images.
- [ ] Scan images for vulnerabilities.
- [ ] Remove unnecessary packages.
- [ ] Keep dependencies updated.
- [ ] Use immutable image tags or digests in production.
Conclusion
Docker container security is not achieved through a single configuration or security mechanism.
It requires multiple layers of protection working together.
Rootless Mode reduces the dependency on host-level root privileges. Seccomp restricts access to unnecessary Linux system calls. AppArmor enforces application-level access policies. Read-only root filesystems prevent unwanted modifications to container files.
When combined with non-root users, restricted Linux capabilities, resource limits, and vulnerability scanning, these controls provide a stronger foundation for production container deployments.
For DevOps engineers, platform engineers, SREs, and backend developers, understanding these security mechanisms is essential when building reliable container-based infrastructure.
Remember: A container should have only the permissions, system calls, filesystem access, and resources required to perform its intended job.
Continue Learning with DevCampus
Want to understand more about Docker, Kubernetes, Linux security, and production infrastructure?
Explore DevCampus for practical engineering tutorials, architecture explanations, and technical learning resources.
Keep learning. Keep building. Build production-ready systems.
DevCampus — Learn Technology. Build Real Skills.
Docker & Container Engineering — Complete Docker learning roadmap covering container internals, security hardening, and hands-on labs.
Docker Container Engineering Career Path — Learn Docker architecture, BuildKit, container security, and Kubernetes migration.
Docker Security: Privileged Containers and Host Takeover — Understand container privilege escalation and Kubernetes security.
OCI Specifications and Container Architecture — Understand OCI, container runtimes, and image specifications.
Interactive Docker Sandboxes — Practise container security and Docker engineering concepts.
Top comments (0)