Skip to content

Beginner · Docker · topic 19 of 53 · 30 min

Image and container security

Explanation

A container is only as safe as its image and the way you run it. Use small, pinned base images, run as a non-root user, keep the file system read-only and drop Linux capabilities you do not need. Never bake secrets into images: use build secrets and runtime secrets. Scan images for known vulnerabilities, rebuild them often, avoid --privileged and the Docker socket, and sign what you publish.

Why container security matters

Containers share the host's kernel, so a container running as root with too many permissions is only a few steps away from controlling the whole server. Images also carry hundreds of packages you did not write, and any of them may have a known vulnerability, often called a CVE (Common Vulnerabilities and Exposures, a public ID for a security bug). Security work therefore has two parts: build a clean image, and run it with the least privilege it needs.

Think of it like this: Think of a hotel room key. A guest gets a key that opens only their room, only during their stay, and not the staff areas. Least privilege means giving each container exactly that kind of key, not the master key to the building.

Build a clean image

  • Minimal base image: slim, alpine or distroless. Fewer packages means fewer vulnerabilities and fewer tools for an attacker. A distroless image has no shell at all.
  • Pin versions: FROM python:3.12-slim, never just python. For full reproducibility, pin the digest too: FROM python:3.12-slim@sha256:<digest>. A digest identifies the exact image content and cannot be moved like a tag.
  • Multi-stage builds, so compilers and dev dependencies never reach the final image.
  • Rebuild and redeploy regularly (for example weekly, from CI) so security patches in the base image reach production. Pinning without updating means you keep old vulnerabilities forever; tools like Dependabot or Renovate can open pull requests to bump pinned versions.

Run as a non-root user

By default, the process in a container runs as root (user ID 0). If an attacker breaks into your app, they are root inside the container, which makes escaping to the host much easier. Create a user in the Dockerfile and switch to it. Many official images already include one: the node images have a user called node.

dockerfile
RUN groupadd --system app && useradd --system --gid app --uid 10001 app
COPY --chown=app:app . /app
USER app
A numeric UID (10001) also lets platforms such as Kubernetes check that the user is not root.

No secrets in images

Everything in an image can be read by anyone who can pull it: files in any layer, ENV values and build ARGs (docker history shows them). So secrets such as a private npm token or an API key must never go into ENV, ARG, or a copied file. For secrets needed DURING the build, use BuildKit build secrets, which are mounted only for one RUN step and never saved in a layer.

dockerfile
# syntax=docker/dockerfile:1
FROM node:24-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci --omit=dev
Build with: docker build --secret id=npmrc,src=$HOME/.npmrc -t shop-api:2.1.0 . The token is used and then gone.

For secrets needed at RUN time, pass them when the container starts: through the platform's secret store (Docker Swarm or Compose secrets, Kubernetes Secrets, AWS Secrets Manager and similar), mounted as files under /run/secrets. Environment variables are common and acceptable for many teams, but remember that they show up in docker inspect.

Run with least privilege

bash
docker run -d --name shop-api \
  --read-only --tmpfs /tmp \
  --cap-drop ALL --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --user 10001 --memory 512m --pids-limit 200 \
  -p 8080:3000 ghcr.io/acme/shop-api:2.1.0
Each line removes something an attacker could use.
OptionWhat it prevents
--read-only (+ --tmpfs /tmp)Writing or changing files in the container, such as dropping a malicious script
--cap-drop ALLLinux capabilities (pieces of root's power, like changing file ownership or network settings). Add back only what you need
--security-opt no-new-privilegesGaining more privileges through setuid programs
--user 10001Running as root, even if the image forgot USER
--memory, --pids-limitOne container exhausting memory or starting endless processes
Careful: Two settings effectively give away the host. --privileged turns off almost all isolation and gives the container access to host devices. Mounting the Docker socket (-v /var/run/docker.sock:/var/run/docker.sock) lets the container control the Docker daemon, so it can start a new privileged container and take over the host. Avoid both, unless you fully trust the image and truly need it, such as a carefully chosen CI agent.

Scan images

bash
docker scout cves ghcr.io/acme/shop-api:2.1.0    # Docker Scout: list known CVEs
trivy image ghcr.io/acme/shop-api:2.1.0           # Trivy, a popular open-source scanner
Run scans in CI and fail the build on critical findings that have a fix available.

A scanner compares the packages in your image with databases of known vulnerabilities. Not every finding is urgent: check whether a fix exists and whether the affected package is actually used. Often the fix is simply rebuilding on a newer base image.

Sign images

How does a server know an image really came from your CI and was not swapped in the registry? By signing it. Cosign, from the Sigstore project, signs an image (by digest) and stores the signature in the registry; at deploy time, cosign verify or a Kubernetes admission policy checks the signature before the image is allowed to run.

Common mistakes

  • Deleting a secret file in a later RUN: it still exists in the earlier layer. Use build secrets.
  • Pinning versions once and never updating them. Pin AND update on a schedule.
  • Ignoring scanner output because there are many findings. Start by fixing critical issues with available fixes, and switch to a smaller base image to cut the noise.
  • Running --privileged "to make it work". Find the one capability or device the app really needs instead.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Image and container security · 1/7

Container security

Containers share the host kernel, so a weak image or a too-powerful container can put the whole server at risk.

Prefer a video by a teacher? Find free videos on “docker security best practices non root image scanning trivy” on YouTube ↗

In real life: Passing a bank's security review

A Mumbai fintech wants to integrate with a large bank, and the bank's security team reviews its deployment. The startup's images used to run as root on a full Debian base, with an npm token in an ARG. They moved to multi-stage builds on node:24-slim, added USER node, used a build secret for the token, and set --read-only and --cap-drop ALL in production. CI now runs Trivy on every image, fails on critical CVEs with fixes, rebuilds weekly, and signs images with cosign. The review passes, and the scanner report becomes part of their audit evidence.

bash
trivy image --severity CRITICAL --ignore-unfixed --exit-code 1 ghcr.io/acme/pay-api:4.2.0
Fails the CI job if a critical vulnerability with an available fix is found.

Practice (0/4 done)

Task 1 · Question

Which Dockerfile line makes the container run as a non-root user?

Task 2 · Code · Python

Write risky_flags(command) that checks a docker run command line and returns a sorted list of finding codes (each code once): - "privileged": the --privileged flag - "docker-socket": any value that contains /var/run/docker.sock - "root-user": -u or --user whose user part (before ":") is root or 0 - "all-capabilities": --cap-add with the value ALL (any case) - "host-network": --network or --net with the value host Flags may be written as "--flag value" or "--flag=value". Split the command on spaces; values never contain spaces.

Task 3 · Practice

Harden this Dockerfile and write the docker run command you would use in production. Explain each change in one line. FROM node WORKDIR /app COPY . . ARG NPM_TOKEN RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && npm install ENV JWT_SECRET=supersecret123 EXPOSE 3000 CMD npm start

Task 4 · Question

A Dockerfile copies .env in one RUN step and deletes it with rm in the next RUN. Is the secret safe?

Quick quiz

Question 1

1.Why is mounting /var/run/docker.sock into a container dangerous?

Question 2

2.What does pinning an image by digest (image@sha256:...) give you?

Question 3

3.Which option set follows least privilege for a normal web API?

Question 4

4.Which tools scan container images for known vulnerabilities?

Question 5

5.You need a private token only while installing packages during the build. What is the right approach?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why is mounting /var/run/docker.sock into a container dangerous?

Answer: It lets the container control the Docker daemon, and so effectively the whole host

With the socket, a container can start a new privileged container that mounts the host file system.

What does pinning an image by digest (image@sha256:...) give you?

Answer: The exact same image content every time, because a digest cannot be moved like a tag

A digest is a content hash. Remember to update pins on a schedule so patches still arrive.

Which option set follows least privilege for a normal web API?

Answer: --read-only --cap-drop ALL --security-opt no-new-privileges

These remove write access, Linux capabilities and privilege escalation that the app does not need.

Which tools scan container images for known vulnerabilities?

Answer: Docker Scout and Trivy

Docker Scout and Trivy compare image packages with vulnerability databases. cosign signs images.

You need a private token only while installing packages during the build. What is the right approach?

Answer: A BuildKit secret mount: RUN --mount=type=secret,...

Secret mounts exist only for that RUN step and are never stored in an image layer or the history.

What is the difference between an image and a container?Related

Answer: An image is the package (recipe); a container is a running copy of it

You build an image once and start one or many containers from it.

From Docker: packaging your app

A container edits a file that came from the image. What happens?Related

Answer: The file is copied into the container's writable layer and changed there; the image is unchanged

Copy-on-write keeps image layers read-only and shared.

From Containers, images and registries

What is Google Cloud's current registry for container images?Related

Answer: Artifact Registry

Artifact Registry replaced Container Registry.

From Containers, images and registries

Which tool is used to sign and verify container images?Related

Answer: cosign

cosign, from the Sigstore project, signs images and verifies signatures.

From Software supply chain security

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Docker”. The module exam comes next.
  2. Module 4 exam: Docker · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Kubernetes fundamentals. 53 topics left in the course before the final exam.

← Previous: Docker Compose: multi-container apps