Use AI to Easily Migrate Your Dockerfiles with Hardened Minimus Images

By
Adam Clark
July 9, 2026

At Minimus, we’ve historically talked about how simple it is to swap out your existing base images and immediately replace them with Minimus images to achieve continuously hardened, near-zero CVE container images.

With the release of our AI Migration skills, that migration path becomes even easier. Teams using AI tooling can now take even complex Dockerfiles, analyze them against Minimus images, and get both practical migration guidance and a converted Dockerfile that preserves application compatibility while improving the security baseline.

Testing Minimus AI Migration Skills on a Non Trivial Dockerfile

To see this in action, I’ve taken a sample application with a multi-stage Dockerfile using a mix of Node and Python-based images to build multiple components of an application.

This is the kind of migration that often looks simple at first, but can quickly involve hidden assumptions around package managers, shell access, user permissions, health checks, and runtime dependencies.

Starting Dockerfile:

FROM node:20-alpine AS frontend

WORKDIR /frontend

COPY frontend/package*.json ./
RUN npm install

COPY frontend/ .

RUN npm run build


FROM python:3.11-alpine

WORKDIR /app

RUN apk add --no-cache \
   gcc \
   musl-dev \
   libpq-dev \
   curl \
   bash

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY api/ .

COPY --from=frontend /frontend/build ./static

ENV FLASK_ENV=production
ENV PORT=5000

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=5s \
 CMD curl -f http://localhost:5000/health || exit 1

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]

Every Minimus Image card comes attached with a sample prompt that can be immediately pasted into your local AI agent of choice, providing specific instructions for analyzing and converting Dockerfiles to use Minimus images.   

After pasting in the prompt to your session, the agent (as instructed) will ask you for the Dockerfile(s) you’d like analyzed and which respective Minimus images you’d like included.  After informing my agent to keep the same types and providing the Dockerfile location, the agent went to work (and quickly did more to provide immediate value than provide just general swaps of the base images).

What the Local AI Agent Found

The AI agent pulled and inspected the relevant images, then validated several important things:

  1. Application compatibility would remain. This was expected because of the way Minimus images are built, but the agent was able to confirm this through its own validation and inspection of what was actually installed on each image.
  2. Minimus images are hardened by default, providing a stronger security baseline than my applications currently have. The AI agent was able to confirm this and recommend any changes needed to ensure the hardening will not break my application.
  3. The Agent supplied recommendations to actually optimize my Dockerfile build.  The agent found I was installing packages via a package manager I thought I needed but I actually did not, and provided recommendations for the removal of those packages from my Dockerfile.

After providing an executive summary and explaining why all of these changes mattered and the impact, the AI Agent provided a new Dockerfile with these recommendations included. It also reported that it had successfully built and tested the updated file.

Read on for a full breakdown of the actual results.  

Migration Executive Summary

Executive Summary

Your Dockerfile needs a genuine multi-stage restructure, not a base-image swap. Both Alpine stages become dev/production Minimus pairs: reg.mini.dev/node:20-dev builds the React app, reg.mini.dev/python:3.11-dev builds the Python venv, and reg.mini.dev/python:3.11 fully distroless, no shell runs the app. Both Minimus lines resolve to the exact same patch versions as your current Alpine images Node 20.20.2, Python 3.11.15, so runtime behavior should be unaffected.

Three concrete changes are required: (1) drop the apk add line entirely and move pip install into the dev builder stage — worth noting, Minimus's Python images are glibc-based rather than musl-based like Alpine, so psycopg2-binary now installs from a prebuilt wheel with no compiler needed at all, which I verified directly; (2) add explicit chown/USER handling since Minimus images default to non-root UID 1000 and your WORKDIR isn't writable by default (I hit this in a real build, see basics/user); (3) replace the curl-based HEALTHCHECK since curl and the shell operator || don't exist in the distroless runtime image (see basics/entrypoint for exec-form requirements).

I built and ran the resulting image end-to-end against your actual frontend/ and api/ code — /health, /api/hello, and the React static bundle all served correctly, and docker exec confirmed there's genuinely no shell in the final image.

Reference guides: Going distroless · Multi-stage builds · Python guide · Package manager access · Image variants

Adaptations Recommended with Explanations

Required Adaptations

Change 1: Split into three build stages (dev builders + distroless runtime)

Summary: Replace FROM node:20-alpine AS frontend and FROM python:3.11-alpine with three stages: reg.mini.dev/node:20-dev (frontend build), reg.mini.dev/python:3.11-dev (Python dependency build), and reg.mini.dev/python:3.11 (final runtime).

Rationale: I extracted the filesystem manifests of both Minimus lines via docker export. The production tags reg.mini.dev/python:3.11 have no /bin/sh and no apk at all — confirmed empty on direct grep. Your current Dockerfile's RUN npm install, RUN npm run build, and RUN apk add ... / RUN pip install ... are shell-form RUN instructions, which Docker always executes via /bin/sh -c. None of that works in the distroless production tag.

The dev tags -dev suffix do carry a shell and apk, per introduction/image-variants and guides/multi-stage-build, and are meant to be discarded — only their output artifacts get copied forward.

Recommendations:

FROM reg.mini.dev/node:20-dev AS frontend
...

FROM reg.mini.dev/python:3.11-dev AS builder
...

FROM reg.mini.dev/python:3.11
COPY --from=builder /app/venv /app/venv
COPY --from=frontend /frontend/build ./static

Why this matters: The three multi-stage build recommendation format gives you dev images used to build your application with the same minimal vulnerability footprint and the ability to copy them over into a final runtime hardened production image with only the necessary artifacts needed to run the application.

Change 2: Drop apk add gcc musl-dev libpq-dev curl bash — install pip deps in the dev stage instead

Summary: Remove line 17-22 RUN apk add --no-cache gcc musl-dev libpq-dev curl bash entirely, and move pip install -r requirements.txt line 25 into the python:3.11-dev builder stage using a venv, copying only the resulting venv/ into the final image.

Rationale: Lines 17-22 exist solely because psycopg2-binary has no musl/Alpine wheel on PyPI, forcing pip to compile from source against libpq-dev/gcc/musl-dev — and none of those tools exist in the distroless runtime tag anyway. I confirmed zero matches for gcc, musl-dev, or libpq across both Minimus Python images.

I tested this directly against reg.mini.dev/python:3.11-dev.

Why this matters: This is an important benefit of the AI migration flow: it does not just replace images. It can identify old build assumptions and remove unnecessary packages that increase image size and attack surface.

FROM reg.mini.dev/python:3.11-dev AS builder
USER root
WORKDIR /app
RUN chown -R 1000:1000 /app
USER 1000
RUN python -m venv venv
ENV PATH="/app/venv/bin:$PATH"
COPY --chown=1000:1000 requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

Why this matters: This is an important benefit of the AI migration flow: it does not just replace images. It can identify old build assumptions and remove unnecessary packages that increase image size and attack surface. 

I ran this against your actual requirements.txtFlask, gunicorn, and psycopg2-binary all installed cleanly with zero compilation.

Change 3: Handle non-root UID 1000 and directory ownership

Summary: Minimus images default to USER 1000 not root. WORKDIR creation and COPY do not automatically make files writable/owned by that user, so build steps that need to write into WORKDIR, like npm install, fail without explicit chown.

Rationale: This isn't theoretical — I hit it directly on the first trial build:

[1/3] STEP 4/6: RUN npm install
npm error code EACCES
npm error path /frontend/node_modules
npm error errno -13

docker inspect confirms "User": "1000" is baked into both reg.mini.dev/node:20-dev and reg.mini.dev/python:3.11-dev, per basics/user, which explicitly documents the “escalate to root temporarily, chown, then switch back” pattern.

Recommendations:

Why this matters: The agent recommended using the documented pattern: temporarily escalate to root, create or change ownership of the required directories, then switch back to the non-root user.

This allows the migrated image to preserve Minimus’ hardened non-root defaults without breaking the application build.

FROM reg.mini.dev/node:20-dev AS frontend
USER root
WORKDIR /frontend
RUN chown -R 1000:1000 /frontend
USER 1000
COPY --chown=1000:1000 frontend/package*.json ./
RUN npm install
COPY --chown=1000:1000 frontend/ .
RUN npm run build

After adding this, the build proceeded and produced a working build/ output — verified by serving it through the final container and confirming GET / returned the compiled React index.html.

Change 4: Replace the curl-based HEALTHCHECK

Summary: Line 36-37 HEALTHCHECK ... CMD curl -f http://localhost:5000/health || exit 1 must be rewritten — it depends on both curl and shell operator ||, neither of which exist in the distroless runtime image.

Rationale: Filesystem export of reg.mini.dev/python:3.11 shows no curl binary anywhere, and confirmed no /bin/sh, so shell-form HEALTHCHECK CMD — which Docker also runs via /bin/sh -c — cannot execute at all. Per basics/entrypoint, any command in a distroless image must use JSON exec-form and can't rely on shell logic like ||.

Recommendations: Since Python is already the runtime, write a tiny stdlib-only health probe and invoke it in exec form:

# api/healthcheck.py
import sys, urllib.request
try:
    with urllib.request.urlopen("http://localhost:5000/health", timeout=3) as r:
        sys.exit(0 if r.status == 200 else 1)
except Exception:
    sys.exit(1)



Sample stdlib-only health probe

HEALTHCHECK --interval=30s --timeout=5s \
  CMD ["python", "healthcheck.py"]

Launching the health probe with the python CMD as part of execution at end of Dockerfile

If you're deploying to Kubernetes rather than plain Docker, the more idiomatic distroless pattern is to drop the Dockerfile HEALTHCHECK entirely and use a livenessProbe/readinessProbe hitting /health directly at the orchestrator level — no in-container binary needed either way.

Why this matters: Where this application runs impacts changes made, as there are benefits to implementing running in standalone VMs versus orchestrators like Kubernetes. The agent has added value here in bringing this awareness up on the chance it was not previously taken into account.

Change 5: CMD already needs no change

Summary: No action needed here — flagging only because Minimus requires exec-form commands.

Rationale: Line 39, CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"], is already valid JSON exec-form with each argument split into its own array element, matching exactly what basics/entrypoint requires. Verified working — docker logs showed gunicorn booting 4 workers and serving requests correctly.

That format is compatible with Minimus distroless images and follows the required entrypoint pattern.

Fully Migrated Dockerfile Provided

FROM reg.mini.dev/node:20-dev AS frontend
USER root
WORKDIR /frontend
RUN chown -R 1000:1000 /frontend
USER 1000
COPY --chown=1000:1000 frontend/package*.json ./
RUN npm install
COPY --chown=1000:1000 frontend/ .
RUN npm run build


FROM reg.mini.dev/python:3.11-dev AS builder
USER root
WORKDIR /app
RUN chown -R 1000:1000 /app
USER 1000
RUN python -m venv venv
ENV PATH="/app/venv/bin:$PATH"
COPY --chown=1000:1000 requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt


FROM reg.mini.dev/python:3.11
WORKDIR /app
COPY --from=builder /app/venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
COPY api/ .
COPY --from=frontend /frontend/build ./static

ENV FLASK_ENV=production
ENV PORT=5000

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=5s \
  CMD ["python", "healthcheck.py"]

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]

Why AI-Assisted Migration Is More Than a Base-Image Swap

This migration showed several important benefits of using AI Migration Skills with Minimus:

  1. A clear and easy migration path for scenarios where organizations may not understand their existing Dockerfiles or have complex Dockerfiles to migrate.
  2. Validation of compatibility when moving to hardened Minimus images. 
  3. Identification of dependencies and changes that may need to be made and of packages that are no longer required in Minimus images.
  4. Recommendations that preserve application behavior while improving the security baseline.
  5. A faster path from existing Dockerfiles to hardened, minimal, near-zero CVE images.

The most important takeaway is that AI Migration Skills make the migration process both faster and smarter. They do not simply suggest a new FROM line. They help evaluate the Dockerfile, validate image compatibility, identify requirements for hardening, and recommend practical changes that reduce unnecessary complexity.

As the consumer of this information, I do not need to accept every recommendation. I can ask the AI agent to account for additional infrastructure or deployment considerations specific to my environment. For example, if I need to keep a shell, run as root, or support a specific operational pattern, I can guide the agent accordingly.

However, if my goal is to achieve a more secure outcome in addition to moving to near-zero CVE images, the AI Migration Skills process helps me get there faster.

AI Migration is more than a workflow for migrating your existing applications to Minimus. It provides a way to validate existing build structures, update Dockerfiles, and quickly transition to hardened images.

Want to know more or try this out?  AI Migration Skills are available on Minimus image skill cards as part of  Minimus Community Edition. Start migrating Dockerfiles to free hardened container images from the Minimus gallery, without registration or procurement friction. Try out the python AI skills migration here.

Adam Clark
Principal Solutions Architect
Sign up for minimus

Avoid over 97% of container CVEs

Access hundreds of hardened images, secure Helm charts, the Minimus custom image builder, and more.