Skip to content

Docker Security Best Practices for Beginners

Practical Docker security for beginners: drop root with USER, avoid --privileged, use minimal images, scan them, keep secrets out of env vars, limit resources, and more.

SDSysadmin Desk August 26, 2026 11 min read
Diagram-style cover showing a hardened Docker container running as a non-root user with a minimal base image, dropped capabilities, and the Docker socket marked as a risk

Docker makes it easy to ship an app, and just as easy to ship one with the defaults left wide open. Most beginner containers run as root, pull whatever’s tagged latest, carry secrets in plain environment variables, and have access to far more of the host than they need. None of that is obvious when the container works, which is exactly why it’s worth fixing before something goes wrong.

The good news is that container security for beginners isn’t about exotic tooling. It’s a handful of habits that each close a common hole: don’t run as root, give the container only what it needs, start from trusted minimal images, keep secrets out of the environment, and watch what you mount. Get these right and you’ve shut the doors attackers actually walk through.

This guide goes through each practice with the commands and Dockerfile changes to apply it. The examples are vendor-neutral and work with a current Docker Engine.

Don’t run containers as root

This is the single most important habit, so it goes first. By default, the process inside a container runs as root, and that root is the same UID 0 as root on the host kernel. The container boundary helps, but if your app has a vulnerability or an attacker finds a breakout, they begin with root’s power instead of a locked-down account.

The fix is a USER directive in your Dockerfile. Create an unprivileged user and switch to it before the app runs:

FROM node:20-slim

# create a non-root user and group
RUN groupadd --gid 10001 app \
  && useradd --uid 10001 --gid app --shell /usr/sbin/nologin app

WORKDIR /app
COPY --chown=app:app . .
RUN npm ci --omit=dev

# drop to the unprivileged user for everything after this
USER app

CMD ["node", "server.js"]

The order matters: do everything that needs elevated rights (installing packages, copying files) first, then USER app, so the actual application process runs unprivileged. If you can’t rebuild the image right now, you can still override the user at runtime:

# run as a specific non-root UID without changing the image
docker run --user 10001:10001 myapp:1.4

Apply least privilege: drop capabilities, never use —privileged

Even a non-root container starts with a set of Linux capabilities it almost certainly doesn’t need. The tight approach is to drop them all and add back only what the app requires:

# drop every capability, then grant only the one you need
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp:1.4

Most web apps need nothing added at all once they bind to a high port. NET_BIND_SERVICE is only needed to bind ports below 1024, and even that you can sidestep by listening on 8080 and mapping it.

At the other extreme is --privileged, and the rule for beginners is simple: don’t. That single flag grants nearly all capabilities and access to host devices, which dismantles most of the isolation Docker gives you. People reach for it to make something work, when the real need is usually one capability or one device.

Privilege flags, from safest to most dangerous

--cap-drop ALL Removes all capabilities. The safe starting point — add back only what's required.
--cap-add NET_BIND_SERVICE Grants exactly one capability. Targeted and reviewable.
--device /dev/... Exposes one specific host device. Use this instead of --privileged when you need hardware access.
--privileged Grants almost everything and host device access. Avoid — it effectively removes container isolation.

Start from official, minimal images

Where your image comes from sets your baseline risk. Pulling a random image from an unknown publisher means trusting their entire build with root on your host. Prefer official images or verified publisher images, and within those, pick the minimal variant.

Smaller base images carry fewer packages, which means fewer known vulnerabilities and less for an attacker to use after a breakout. A full OS image ships a shell, package managers, and dozens of libraries your app never calls. Slim and distroless variants strip that down.

Base image choices, roughly fewest extras first

distroless / scratch Only your app and its runtime — no shell, no package manager. Smallest attack surface, but harder to debug inside.
alpine Tiny Linux base with a shell. Popular for size; occasionally has musl-vs-glibc quirks.
-slim variants (e.g. debian:bookworm-slim) Trimmed standard distro. A good balance of small and familiar.
full OS images Everything included. Convenient but the largest surface — avoid for production unless you need it.

Just as important: pin a specific tag, never latest. latest is a moving target — the image under it changes without warning, so your “tested” build and tomorrow’s deploy can be different software. Pin to a real version, and for stronger guarantees, pin by digest:

# avoid: a moving, unpredictable target
FROM node:latest

# better: a specific version
FROM node:20.11-slim

# strongest: an immutable digest
FROM node:20.11-slim@sha256:abc123...

If you’re choosing where to run all this, the install Docker on Ubuntu Server walkthrough sets up the engine on a clean host so you’re not layering containers on a half-configured system.

Scan your images for vulnerabilities

Choosing a good base image gets you a clean start; scanning tells you when it stops being clean. A scanner inspects the packages in an image and reports known CVEs by severity. Run one before you deploy, and again on a schedule, because new vulnerabilities get disclosed against images you haven’t touched.

# built into recent Docker: scan a local image
docker scout cves myapp:1.4

# Trivy — a widely used standalone scanner
trivy image myapp:1.4

# Grype — another popular option
grype myapp:1.4

Treat the results as a priority list, not a pass/fail. Focus on the critical and high findings that have a fix available, rebuild on an updated base to clear them, and don’t get paralyzed by a long tail of low-severity issues with no patch yet.

Keep secrets out of environment variables

Putting a password or API key in an environment variable is the most common secret-handling mistake, because it looks like it works and the leak is invisible. Environment variables show up in docker inspect, in the process environment, often in logs, and they’re inherited by any child process the app spawns.

# anyone who can run this reads your secret
docker inspect myapp | grep -i password

Instead, pass secrets as files the app reads, or use the secrets mechanism of your orchestrator. With Compose, you can mount a secret as a file rather than baking it into the environment:

services:
  app:
    image: myapp:1.4
    secrets:
      - db_password
    environment:
      # point the app at the file, not at the secret itself
      DB_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

The application reads /run/secrets/db_password at startup. The value never sits in the environment, never appears in docker inspect, and the file lives in a tmpfs that isn’t written to the image layers. Keep the source file out of git with a .gitignore entry, the same way you would a .env.

Keep Docker and your images updated

Old software is the easiest target there is. That applies to the Docker Engine itself, the host kernel, and the images you run. Keep the host’s Docker packages current through its package manager, and rebuild your images regularly so they pick up upstream security fixes — a base image you built six months ago is six months behind on patches even if your code never changed.

A simple rhythm works: update the host on its normal patch cycle, rebuild images when their base publishes security updates, and re-scan after each rebuild to confirm the known issues actually cleared.

Limit what a container can consume

Resource limits are a security control, not just a performance knob. A container with no limits can exhaust the host’s memory or CPU — sometimes through a bug, sometimes because a compromised container is mining crypto or running a fork bomb — and take every other container down with it.

# cap memory and CPU for a container
docker run -d \
  --memory 512m \
  --cpus 1.0 \
  --pids-limit 200 \
  myapp:1.4

--memory caps RAM, --cpus caps CPU time, and --pids-limit caps how many processes the container can spawn, which blunts fork-bomb style abuse. In Compose, the same limits go under a deploy.resources.limits block. Set them to a bit above what the app actually needs so a runaway container hits a ceiling instead of starving the host.

Be very careful with the Docker socket

Mounting the Docker socket into a container looks harmless and is one of the most dangerous things you can do. The socket at /var/run/docker.sock is the control channel for the Docker daemon, and the daemon runs as root. A container that can talk to the socket can start new containers, mount the host’s entire filesystem, and from there own the machine.

# this gives the container effective root on the host — avoid
docker run -v /var/run/docker.sock:/var/run/docker.sock somimage

Plenty of monitoring and management tools ask for it. Before granting it, treat the request as “this container wants root on my host,” because functionally that’s what it is. If a tool genuinely needs to talk to Docker, look for a read-only socket proxy that exposes only the specific API calls it requires, rather than handing over the raw socket.

A starter security checklist

None of these takes long on its own, and together they cover the mistakes that show up most often in beginner setups.

Before you run a container in anything that matters

  • App runs as a non-root user (USER directive or --user)
  • Capabilities dropped with --cap-drop ALL, only needed ones added back
  • No --privileged flag anywhere
  • Built from an official, pinned, minimal base image (not latest)
  • Image scanned, with critical/high fixable CVEs addressed
  • Secrets mounted as files or via a secrets manager, never in env vars or image layers
  • Memory, CPU, and PID limits set
  • Docker socket not mounted unless genuinely required (and then via a proxy)

Security isn’t a flag you flip once — it’s the result of a few defaults you change and then stop thinking about. Run as non-root, start minimal and pinned, drop privileges, keep secrets out of the environment, and never mount the socket casually. Do that and your containers are in far better shape than the average first deployment.

From here, the natural next steps are understanding how data persists safely with bind mounts vs named volumes and managing multi-container apps cleanly with the Docker Compose beginner guide, where the secrets and resource-limit patterns above slot right in. For more container walkthroughs, browse the Docker guides.

Frequently asked questions

Why shouldn't containers run as root?

By default the process inside a container runs as root, and that root maps to root on the host kernel. If an attacker breaks out of the container or exploits a bug in your app, they start with root-level power instead of an unprivileged account. Adding a USER directive so the app runs as a normal user limits what a compromise can reach.

Is --privileged ever safe to use?

Rarely, and almost never for application containers. The --privileged flag hands the container nearly all kernel capabilities and access to host devices, which effectively removes the isolation Docker provides. If you only need one capability, grant just that one with --cap-add instead of opening everything.

What's wrong with putting secrets in environment variables?

Environment variables are easy to leak. They show up in 'docker inspect', in process listings, in logs, and they're inherited by child processes. Anyone who can run docker inspect on the container can read them. Use Docker secrets, a secrets manager, or mounted files with tight permissions instead.

How do I scan a Docker image for vulnerabilities?

Use a scanner like 'docker scout', Trivy, or Grype against the image. It compares the installed packages against vulnerability databases and reports known CVEs by severity. Scan before you deploy and again periodically, since new vulnerabilities are disclosed against images that haven't changed.

Why is mounting the Docker socket dangerous?

The Docker socket (/var/run/docker.sock) controls the Docker daemon, which runs as root. A container with the socket mounted can start new containers, mount the host filesystem, and effectively take over the host. Treat socket access as equivalent to giving root on the machine, and avoid it unless absolutely necessary.

Does using official images make my container secure?

It's a good start, not a guarantee. Official and verified images are maintained and less likely to contain malicious code than random ones, but they still ship with vulnerabilities over time and can include more than you need. Prefer official minimal or slim variants, pin a specific tag, and scan them regularly.

Sources & further reading

Official vendor documentation referenced while writing this guide.

SD

Sysadmin Desk

Infrastructure & Cloud

Hands-on guidance for infrastructure, virtualization, and containers — Hyper-V, VMware, Docker, and the day-to-day operations work that keeps environments running.

MCSA Guru provides independent, educational IT guidance. Microsoft, Windows, Windows Server, Microsoft 365, Exchange, and Microsoft Teams are trademarks of Microsoft Corporation; Docker is a trademark of Docker, Inc. MCSA Guru is not affiliated with or endorsed by Microsoft or Docker. Always test changes in a safe environment before applying them in production.

Related guides

Diagram-style cover showing a temporary container archiving a Docker named volume to a tar file on the host

How to Back Up Docker Volumes

Back up Docker named volumes the reliable way: tar a volume via a temp container, restore it, dump databases properly, schedule it, and move data between hosts.

Sysadmin Desk Jul 19, 2026 8 min read

Fixing something right now?

Jump straight into the guide library or search for the exact error or task you are dealing with.