Docker Container Security: Non-Root Users, Scanning with Trivy
Practice4 minInnovialabs editorial team
In short
Run containers as a non-root user from a minimal image, make the filesystem read-only, drop all Linux capabilities, never mount the Docker socket, and fail the CI build when Trivy finds fixable high or critical CVEs.
The short answer
A container is not a security boundary by default. It shares the host kernel, and a typical image runs as root with a full set of default capabilities. Most real-world hardening comes down to five habits:
- run the process as a non-root user;
- build from a minimal base image;
- make the root filesystem read-only;
- drop Linux capabilities you do not need (usually all of them);
- never mount the Docker socket into an application container.
On top of that, scan every image for known vulnerabilities (CVEs) in CI, so a vulnerable dependency is caught before it reaches production.
Non-root user and minimal image
If an attacker exploits your application, they get the privileges of its process. Root inside a container is much closer to root on the host than most people assume, especially combined with a misconfiguration.
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Key points:
- USER switches to an unprivileged account. Official Node images ship a
nodeuser; for others, create one withuseraddor use a numeric UID. - Application files are owned by root and only readable by the app user, so the process cannot rewrite its own code.
- Listen on a port above 1024, so no extra privileges are needed to bind it.
- Prefer slim, Alpine or distroless bases and multi-stage builds: compilers, package managers and shells stay in the build stage. Fewer packages mean fewer CVEs and fewer tools for an attacker.
Read-only filesystem and dropped capabilities
Most services only need to write to a temporary directory and maybe one data volume. Everything else can be read-only.
docker run -d \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
myapp:1.4.2
- --read-only blocks an attacker from dropping binaries or modifying configs.
- --tmpfs /tmp gives the app a writable scratch space in memory.
- --cap-drop ALL removes kernel capabilities like changing file ownership or raw network access. Add back only what you can justify.
- no-new-privileges stops setuid binaries from escalating privileges.
In Kubernetes the same ideas live in the container securityContext:
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
Never mount the Docker socket
Mounting /var/run/docker.sock into a container gives it full control of the Docker daemon. Whoever controls the daemon can start a privileged container with the host filesystem mounted, which is effectively root on the host. Treat the socket like a root password.
If a tool really needs to talk to Docker (CI runners, log collectors), isolate it on a dedicated host, use a filtering proxy with a narrow allowlist, or switch to tools that build images without a daemon. The same logic applies to --privileged and --pid=host: avoid them in application containers.
Scanning images with Trivy in CI
Trivy is an open-source scanner that checks OS packages and language dependencies in an image against vulnerability databases. It can also check Dockerfiles and Kubernetes manifests for misconfigurations.
# fail the pipeline on fixable HIGH/CRITICAL vulnerabilities
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 myapp:1.4.2
# check Dockerfile and manifests for misconfigurations
trivy config .
How to make scanning useful rather than noisy:
- Gate on severity: block merges on HIGH and CRITICAL, report the rest.
- --ignore-unfixed skips CVEs that have no patch yet; you cannot act on them anyway.
- Keep accepted risks in a .trivyignore file with a comment and a review date for each entry.
- Rescan regularly, not only on build: new CVEs appear for images that have not changed.
- Pin third-party CI actions to a specific version or commit, since the scanner itself is part of your supply chain.
Common mistakes
| Mistake | Better |
|---|---|
latest tags in production | Pin versions or digests |
| Secrets baked into image layers | Pass secrets at runtime or via build secrets |
| One big image with build tools | Multi-stage build, slim runtime |
| Scanning once and forgetting | Scheduled rescans of deployed images |
--privileged "to make it work" | Find and add the single capability needed |