How to Migrate Your Existing Project to Docker Hardened Images: A Practical Guide

Migrating to Docker Hardened Images is one of the highest-impact, lowest-effort security improvements you can make. One FROM line in your Dockerfile. That's all it takes.

How to Migrate Your Existing Project to Docker Hardened Images: A Practical Guide

Say you have an existing GitHub repo project that uses a Dockerfile — maybe it's a Node.js API, a Python Flask app, or a Go microservice. It works. It builds. It runs in production. But one day you run a security scan and the results aren't pretty: dozens of CVEs in your base image, half of which you've never even heard of. Packages you didn't install. Libraries your app doesn't need. Attack surface you didn't ask for.

Sound familiar? This is the reality for most containerized applications running on standard base images.

Now, what if you could swap out that base image for one that's been stripped down, security-hardened, and continuously maintained — without changing your application code or your build workflow? That's exactly what Docker Hardened Images (DHIs) offer, and this guide will walk you through the migration step by step.

What Are Docker Hardened Images, and Why Should You Care?

Before we jump into the migration, let's understand what you're migrating to.

Docker Hardened Images are curated base images maintained by Docker with three goals in mind. First, they're secure by default — unnecessary packages, tools, and libraries are stripped out to minimize the attack surface. Second, they have a minimal footprint — you get only what your application needs to run, which means fewer CVEs showing up in your scan reports. Third, they're continuously maintained — Docker actively monitors these images and pushes patches when vulnerabilities are discovered, so you're not left scrambling when the next Log4j-level event hits.

For teams working in regulated industries like finance, healthcare, or government, this isn't just a nice-to-have. It's a compliance requirement. But even if you're a solo developer shipping a side project, starting with a hardened base image is one of the easiest wins for your container security posture.

The Distroless Advantage: Why DHIs Are Different

💡
Docker Hardened Images are built on a "distroless approach" — and this is what sets them apart from simply picking a slim or alpine variant of a standard image. Traditional base images ship with shells, package managers, debugging tools, and a bunch of OS-level utilities that your application never touches. A distroless image strips all of that away, keeping only the minimal OS components your app actually needs to run.

Here's what that translates to in practice:

Drastically Reduced Attack Surface

When you remove shells, package managers, and debugging tools from a container image, you eliminate up to 95% of the traditional container attack surface. No shell means an attacker who breaches the container can't drop into a bash session. No package manager means they can't install additional tools. Every component you remove is one less potential entry point.

Improved Operational Efficiency

Fewer packages means smaller images, and smaller images have a ripple effect across your entire workflow. Pull times drop significantly, which speeds up deployments — especially in auto-scaling scenarios where new containers need to spin up fast. Storage costs go down because you're not warehousing hundreds of megabytes of unused libraries. And the minimal footprint can translate to improved runtime performance since there's less overhead competing for resources.

Reduced Vulnerability Noise

Here's a principle that sounds obvious but gets overlooked constantly: less software means fewer vulnerabilities. When your base image ships with hundreds of packages you didn't ask for, every one of them is a potential CVE waiting to show up in your next scan. Platform engineers and security teams end up drowning in "patch panic" — triaging vulnerabilities in tools their application never even calls. DHIs cut through that noise. You see fewer CVEs because there are genuinely fewer components to be vulnerable.

Compliance Built In

This is where DHIs really shine for enterprise teams. These images come with signed metadata that makes audits significantly less painful. Each image includes an SBOM (Software Bill of Materials) so you can verify exactly what's inside the image, down to every package and dependency. They include VEX (Vulnerability Exploitability eXchange) data to distinguish between vulnerabilities that exist in the image versus those that are actually exploitable in your context. And they provide provenance information that proves how and where the image was built — critical for supply chain security requirements.

Broad Compatibility

You might be wondering: if all that stuff is stripped out, will my application actually work? The answer is yes. DHIs are designed to maintain compatibility with common application needs. They offer variants with musl libc (Alpine-based) for teams that prioritize minimal size, and glibc (Debian-based) for applications with broader library dependencies. So you're not trading functionality for security — you're getting both.

Prerequisites

Before you begin the migration, make sure you have the following in place:

  • An active Docker subscription. Docker Hardened Images are available to users on Docker Business or Teams plans. Check your subscription status in your Docker account settings.
  • Docker Hub access. You'll need to sign in to Docker Hub and have an organization with DHI access.
  • Your existing project with a Dockerfile. That's the repo we're going to migrate.

Step 1: Audit Your Current Dockerfile

Let's start by looking at what you're working with. Open up your project's Dockerfile and identify the base image. Here's a common example for a Node.js application:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

This looks clean enough, right? But node:20-alpine ships with more than just Node.js. Run a quick scan to see what's hiding in there:

docker scout quickview node:20-alpine

You'll likely find a list of CVEs — some critical, some high severity — most of which have nothing to do with your application. They come from packages baked into the base image that your app never touches.

This is the problem DHIs solve. They give you the runtime you need without the baggage.

Step 2: Browse the DHI Catalog

Now let's find the hardened equivalent of your current base image. Head over to Docker Hub and follow these steps:

  • Sign in to Docker Hub.
  • Navigate to My Hub from the top navigation.
  • Select your organization that has DHI access from the namespace drop-down menu.
  • Go to Hardened Images > Catalog.

The catalog lets you search and filter images based on your technology stack. Looking for a hardened Node.js image? Search for it. Need an Alpine-based variant for a smaller footprint, or a Debian-based one for broader compatibility? The catalog has you covered.

Take note of the exact image name and available tags — you'll need these for the next step.

Step 3: Update Your Dockerfile

This is where the actual migration happens, and it's surprisingly simple. You're essentially replacing the FROM line in your Dockerfile with the hardened equivalent.

Before (standard base image):

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

After (Docker Hardened Image):

FROM <your-org>/hardened-node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

That's it. One line changed. Everything else stays the same — your COPY instructions, your RUN commands, your CMD — all untouched.

Here's a pro tip: Docker AI can automate this for you. If you're not sure which hardened image maps to your current base image, Docker AI can analyze your Dockerfile and suggest the appropriate replacement. This is especially handy when you're migrating multiple services in a larger project.

What About Multi-Stage Builds?

If your project uses multi-stage builds (and it should, for production), you'll want to update the final stage's base image. Here's an example for a Go application:

# Build stage — this can stay as-is
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .

# Runtime stage — this is what you harden
FROM <your-org>/hardened-alpine:3.19
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]

The build stage doesn't need hardening — it's a throwaway environment. But the runtime stage is what actually runs in production, and that's where DHIs make the biggest difference.

Step 4: Build, Test, and Validate

With your updated Dockerfile, build the image and make sure everything works:

docker build -t my-app:hardened .
docker run -d -p 3000:3000 my-app:hardened

Run your test suite against the container to catch any compatibility issues. In most cases, the migration is seamless because DHIs maintain the same runtime interfaces as their standard counterparts. But it's always good practice to verify.

Now, run another security scan on your new image:

docker scout quickview my-app:hardened

Compare the results with what you saw earlier. You should see a significant reduction in CVEs — that's the hardening doing its job.

Step 5: Update Your CI/CD Pipeline

If your project builds and deploys through a CI/CD pipeline (GitHub Actions, GitLab CI, Jenkins, etc.), update the pipeline configuration to use the hardened image. Here's an example for a GitHub Actions workflow:

name: Build and Push

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/my-app:latest

Nothing changes in the pipeline itself — the hardened base image is referenced in the Dockerfile, so the pipeline just picks it up automatically. But this is a good opportunity to add a security scan step if you don't already have one.

Step 6: Keep Your Images Updated

Here's the part most teams overlook. Migrating to DHIs isn't a one-and-done move. Docker continuously patches and updates these images, and it's your responsibility to pull the latest versions regularly.

Set up automated base image update checks in your pipeline. Tools like Dependabot or Renovate can monitor your Dockerfile for base image updates and open pull requests automatically when new versions are available.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: docker
    directory: "/"
    schedule:
      interval: weekly

This way, you're always running on the latest hardened image without having to remember to check manually.

Understanding the Shared Responsibility Model

Docker Hardened Images operate on a shared responsibility model, and it's important to understand where Docker's responsibility ends and yours begins.

Docker handles maintaining the hardened images, scanning for vulnerabilities, applying security patches, and ensuring the images meet compliance standards. They take care of the base layer so you don't have to.

You're responsible for keeping your images updated to the latest hardened versions, ensuring your application code and dependencies are secure, validating that your deployment configurations meet your security requirements, and making sure your CI/CD pipelines pull fresh images on a regular cadence.

Think of it like renting a secure office space. The building management handles the locks, security cameras, and fire alarms. But you're responsible for not leaving sensitive documents on the lobby table.

Wrapping Up

Migrating an existing Dockerized project to Docker Hardened Images is one of the highest-impact, lowest-effort security improvements you can make. You change one line in your Dockerfile, and you get a dramatically reduced attack surface, fewer CVEs, and continuous security maintenance from Docker.

The workflow doesn't change. The build process doesn't change. Your application code doesn't change. What changes is the foundation your containers run on — and that foundation is now built for security from the ground up.

If you've been putting off container security because it felt like a massive overhaul, this is your sign to start. One FROM line. That's all it takes.

Resources