Skip to content
Innovialabs

Docker Container Security: Non-Root Users, Scanning with Trivy

Practice4 min

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 node user; for others, create one with useradd or 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

MistakeBetter
latest tags in productionPin versions or digests
Secrets baked into image layersPass secrets at runtime or via build secrets
One big image with build toolsMulti-stage build, slim runtime
Scanning once and forgettingScheduled rescans of deployed images
--privileged "to make it work"Find and add the single capability needed

FAQ

Not always. Alpine is small, but it uses musl instead of glibc, which can cause compatibility issues for some languages and native modules. Slim Debian-based or distroless images are often just as good. What matters most is fewer packages and regular updates.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.