Master Container Security in 2025 — Best Practices & Live Demo
By Vladimir Mikhalev · Docker Captain · CNCF Ambassador · IBM Champion
This is a practical guide to container security. Protecting your applications, securing your supply chain, and staying ahead of new threats.
It’s 2025. The threats are more frequent now, smarter, and they do more damage. Compromised servers and stolen databases used to be the whole story. Not anymore. Container security sits at the center of it. In CNCF’s 2024 annual survey, 91% of responding organizations used containers in production.
Container adoption keeps growing, and the security risks grow with it. Attackers know this. So every one of us, whether you write code, run security, design systems, or do DevOps, has to take container security seriously.
Here’s what we’ll cover:
- Why container security matters in 2025
- Key best practices that every team should follow
- Practical examples using Docker Scout for local scanning and Snyk for continuous security checks in GitHub Actions.
Time is short. Let’s get into it.
Container Security in 2025
Why does any of this matter in 2025? Look at the architecture. Microservices are everywhere. A single application might be dozens or hundreds of containers, all talking to each other. That blows up your attack surface.
The reason is simple. Every container is its own little environment, with its own OS packages, libraries, and config. One misconfigured container, or one running unpatched software, is enough. That single weakness can turn into a major breach.
Supply chain attacks are climbing too. Attackers stopped going after your code alone. Now they go after whatever you depend on: base images, external libraries, third-party integrations.
Put plainly, the container image itself is a place where things can go wrong. So we have to pay attention to:
- What’s in our base image
- Our runtime environment
- The pipeline that builds and ships these containers
My goal here is to give you a practical way to shift security left. Catch issues early in development, where fixing them is cheap, and bake the checks into the work you already do every day
Key Best Practices in Container Security
So how do you actually secure containers? Below are seven core principles. They apply to Docker, to Kubernetes, and to any containerized setup you run.
1. Use Minimal Base Images
Bigger images carry more dependencies. More dependencies means more for an attacker to chew on. Reach for lightweight images like Alpine or distroless instead of a full Linux distribution whenever you can.
Consider the size. Alpine is around 5 megabytes. Ubuntu or Debian run tens or hundreds of megabytes.
Fewer packages, fewer security risks, smaller attack surface. That’s the whole point.
2. Pin Your Versions
When you pull an image, don’t write this:
FROM python:3Be specific. A version tag is a start:
FROM python:3.13-alpineA tag only names a version, though. The publisher can move it to a new build at any time, and every rebuild of yours then pulls something different. What locks the image is its digest:
FROM python:3.13-alpine@sha256:sha256:2d9aefe2fef018a7eb2c13064c89c71929800fd2e5dccdbf52ea5da5bb8d929aDocker pulls exactly that content, even if the tag is later pointed somewhere else. Keep the tag in front of the digest for humans, get the digest with docker buildx imagetools inspect python:3.13-alpine, and let a bot such as Dependabot or Renovate move it on purpose. And never use latest as a tag. Pull FROM python:latest and you have no idea what you’re actually getting. It can shift under you and break your app.
Worse, if a tag you pull is ever repointed to a poisoned image, your system pulls it without anyone noticing. A digest pin does not move.
Pin by digest. Stay in control.
3. Scan Early and Often
Scanning is not a one-and-done step. You scan for risk at every stage:
✅ Development
✅ Staging
✅ Production
Tools like Docker Scout and Snyk handle this for you.
Take Docker Scout. One command scans an image and shows you the risks right there.
Build security into your pipeline on day one. Not as a patch you bolt on at the end.
4. Use Multi-Stage Builds
Leftover dependencies, libraries, and build tools sitting in your container? That’s more surface for an attacker to use.
Multi-stage builds fix this. Build your app in one stage, then copy only what you need into a final, smaller image.
That strips out:
🚫 Debugging tools
🚫 Compilers
🚫 Temporary files
What you’re left with is smaller, cleaner, and harder to attack.
5. Drop Unnecessary Privileges
Containers run as root by default. That’s a big problem. An attacker who takes over a root container can move out across your system.
Run as a non-root user instead. Specify it like this:
USER 1000:1000What does that actually do?
- The first one thousand is the user ID. Just a regular non-root user in Linux.
- The second one thousand is the group ID. GID 1000 is an ordinary group with no special privileges; the point is that neither ID is 0.
Run as a low-privilege user and a break-in stays contained. No system-wide access for whoever got in.
6. Keep Secrets Out of Images
Don’t bake sensitive data into an image. It’s a common mistake, and it leaks your secrets the moment the image gets pulled by the wrong person or shared where it shouldn’t be.
Keep secrets somewhere safe instead:
✅ Secrets mounted as files at runtime: Docker secrets or Kubernetes Secrets mounted as a volume
✅ Build-time secrets passed with BuildKit’s RUN --mount=type=secret, which never land in an image layer
✅ A secrets manager like AWS Secrets Manager or HashiCorp Vault
Plain environment variables are not a safe home for secrets: anyone who can run docker inspect on the container, or read the process environment, can read them.
And if a secret does get hard-coded by accident, tools like TruffleHog or GitLeaks will catch it before it leaks.
No secrets in images. None in repos. None in logs.
7. Monitor Runtime Behavior
Build-time scanning only gets you so far. You also have to watch what’s actually happening in production.
Falco and Sysdig catch suspicious activity inside running containers.
Here’s a concrete case. Falco can alert you the moment a container starts a shell out of nowhere, which often means someone is now driving it.
Other things to watch for:
🚨 Unexpected file modifications
🚨 High network activity from a container
🚨 Containers trying to escalate privileges
Watch the runtime and you catch threats while they’re still small, before they turn into an incident.
Final Thoughts on Key Best Practices in Container Security
Follow these seven principles and you’re already ahead of plenty of organizations still fumbling container security.
Less is more here. Less complexity, lower privileges, fewer secrets. That’s a more secure container.
Docker Scout for Local Image Scanning
Enough theory. Let’s run Docker Scout for real.
Docker Scout scans your Docker images for security issues and hands you best-practice recommendations.
Before we start, make sure you have:
- Docker Desktop 4.38 or later installed
- A Docker Account, and that you’re signed in using
docker login
Say we have a sample Node.js application. This is the Dockerfile.
# Pin the base image by digest. The tag is for humans; the digest is what Docker pulls.FROM node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402
# Set the working directory inside the containerWORKDIR /usr/src/app
# Copy only the package files first (for efficient layer caching)COPY package*.json ./
# Install exactly what the lockfile says, without dev dependenciesRUN npm ci --omit=dev
# Copy the application sourceCOPY . .
# Drop root: the node image ships an unprivileged "node" userUSER node
# Expose port 3000 for the Node.js serverEXPOSE 3000
# The default command to run the appCMD ["node", "index.js"]I’m using node:22-alpine, pinned by digest, because it’s smaller than node:22. Better for containers. Smaller doesn’t mean safe, though, so we still scan.
Now build the image.
docker build -t my-node-app:1.0.0 .That packages the application into a Docker image named my-node-app, version 1.0.0.
Next, point Docker Scout at it.
docker scout cves my-node-app:1.0.0Docker Scout analyzes the image and flags whatever looks risky.
When the scan finishes, you get a report. It lists the issues it found, their severity, and how to fix them.
When I re-ran this demo on 2026-10-07, the scan indexed 212 packages and flagged 10 of them, with no critical findings and 12 rated high. Every flagged package was part of the npm CLI that ships inside the base image, not a dependency of the app, which has none. That is a common next step: a multi-stage build whose final stage carries the runtime without the package manager.
Those issues might trace back to system libraries, base packages, or your own application dependencies. Docker Scout will often tell you to upgrade a component or apply a patch.
Why bother locally? Because you catch problems here, before the image ever hits a registry or a production cluster. Fix them now and your environment is more secure immediately.
That’s the short version of using Docker Scout to tighten up your containerized applications.
Updated on 2026-10-07: pinning now uses image digests, the secrets list no longer includes environment variables, and the demo Dockerfile runs as a non-root user and installs with npm ci. The Docker Scout demo was re-run that day with Docker Engine 29.7.2. The video shows the original 2025 version.
Snyk in GitHub Actions
Let’s take it further and automate scanning in the pipeline with Snyk and GitHub Actions.
Before we start, make sure you:
-
Sign up for a free Snyk account at snyk.io.
-
Go to your Snyk account settings page, find your Access Token (Key), and copy it.
-
In your GitHub repo, go to Settings → Secrets and variables → Actions, and create a new secret named
SNYK_TOKEN.
Say we have a Node.js project. The goal is straightforward. A developer pushes code or opens a pull request, and Snyk scans for security issues on its own.
- Snyk will check the container image to detect risks in the base image and OS packages.
- It will also check application dependencies to find security flaws in
package.jsonand other libraries.
Create a GitHub Action Workflow
For this we need a new GitHub Actions workflow file. It goes inside the .github folder, then workflows, and we name it snyk.yml.
Updated October 2026: the action references below are pinned to released tags. In production, pin them to commit SHAs so the workflow cannot change underneath you.
name: Snyk Security Scan
on: push: branches: ["main"] pull_request: branches: ["main"]
jobs: security: runs-on: ubuntu-latest steps: - name: Check out the repository uses: actions/checkout@v4
- name: Set up Docker Buildx uses: docker/setup-buildx-action@v3
- name: Build Docker image run: docker build -t my-node-app:1.0.0 .
- name: Snyk Container Scan env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: image: "my-node-app:1.0.0" args: "--file=Dockerfile"
- name: Snyk Code & Dependency Scan env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: "--file=package.json"This workflow scans both the Docker image and the application dependencies for risk, automatically. Notice it pulls SNYK_TOKEN from GitHub Secrets. That’s the right way to do it. Your credentials stay out of the code, and nothing leaks by accident.
Wrap-Up and Final Tips
We got through a lot:
- Why container security matters in 2025
- Seven core principles, from minimal images to least privilege
- A live demo of Docker Scout for local image scanning
- How to automate security scans using Snyk in GitHub Actions
But here’s the key takeaway: Container security isn’t just about tools or settings. It’s a mindset. Everyone on your team plays a role in keeping things secure.
A few more practical things you can do right now:
- Monitor runtime with tools like Falco or Sysdig to detect suspicious activity.
- Keep everything up to date: patching OS packages and dependencies is one of the simplest and most effective security steps.
- Enforce security policies, like blocking deployments if there are serious security issues. A single rule like this can make a big impact.
- Educate your team: security isn’t just for DevOps or engineers; it’s a shared responsibility across developers, QA, and security teams.
Thank you for reading! Don’t forget to check out the video version for additional insights and visuals.