Migrate an existing application to use Docker Hardened Images

Subscription: Docker Hardened Images

This guide helps you migrate your existing Dockerfiles to use Docker Hardened Images (DHIs) manually, or with Gordon. DHIs are minimal and security-focused, which may require adjustments to your base images, build process, and runtime configuration.

This guide focuses on migrating framework images, such as images for building applications from source using languages like Go, Python, or Node.js. If you're migrating application images, such as databases, proxies, or other prebuilt services, many of the same principles still apply.

Migration considerations

DHIs omit common tools such as shells and package managers to reduce the attack surface. They also default to running as a nonroot user. As a result, migrating to DHI typically requires the following changes to your Dockerfile:

ItemMigration note
Base imageReplace your base images in your Dockerfile with a Docker Hardened Image.
Package managementImages intended for runtime, don't contain package managers. Use package managers only in images with a dev tag. Utilize multi-stage builds and copy necessary artifacts from the build stage to the runtime stage.
Non-root userBy default, images intended for runtime, run as the nonroot user. Ensure that necessary files and directories are accessible to the nonroot user.
Multi-stage buildUtilize images with a dev or sdk tags for build stages and non-dev images for runtime.
TLS certificatesDHIs contain standard TLS certificates by default. There is no need to install TLS certificates.
PortsDHIs intended for runtime run as a nonroot user by default. As a result, applications in these images can't bind to privileged ports (below 1024) when running in Kubernetes or in Docker Engine versions older than 20.10. To avoid issues, configure your application to listen on port 1025 or higher inside the container.
Entry pointDHIs may have different entry points than images such as Docker Official Images. Inspect entry points for DHIs and update your Dockerfile if necessary.
No shellDHIs intended for runtime don't contain a shell. Use dev images in build stages to run shell commands and then copy artifacts to the runtime stage.

For more details and troubleshooting tips, see the Troubleshoot.

Migrate an existing application

The following steps outline the migration process.

Step 1: Update the base image in your Dockerfile

Update the base image in your application’s Dockerfile to a hardened image. This is typically going to be an image tagged as dev or sdk because it has the tools needed to install packages and dependencies.

The following example diff snippet from a Dockerfile shows the old base image replaced by the new hardened image.

- ## Original base image
- FROM golang:1.22

+ ## Updated to use hardened base image
+ FROM <your-namespace>/dhi-golang:1.22-dev

Step 2: Update the runtime image in your Dockerfile

メモ

Multi-stage builds are recommended to keep your final image minimal and secure. Single-stage builds are supported, but they include the full dev image and therefore result in a larger image with a broader attack surface.

To ensure that your final image is as minimal as possible, you should use a multi-stage build. All stages in your Dockerfile should use a hardened image. While intermediary stages will typically use images tagged as dev or sdk, your final runtime stage should use a runtime image.

Utilize the build stage to compile your application and copy the resulting artifacts to the final runtime stage. This ensures that your final image is minimal and secure.

See the Example Dockerfile migrations section for examples of how to update your Dockerfile.

Example Dockerfile migrations

The following examples show a Dockerfile before and after migration. Each example includes both a multi-stage build (recommended for minimal, secure images) and a single-stage build (supported, but results in a larger image with a broader attack surface).

メモ

Multi-stage builds are recommended for most use cases. Single-stage builds are supported for simplicity, but come with tradeoffs in size and security.

Go example

#syntax=docker/dockerfile:1

FROM golang:latest

WORKDIR /app
ADD . ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-s -w" --installsuffix cgo -o main .

ENTRYPOINT ["/app/main"]
#syntax=docker/dockerfile:1

# === Build stage: Compile Go application ===
FROM <your-namespace>/dhi-golang:1-alpine3.21-dev AS builder

WORKDIR /app
ADD . ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-s -w" --installsuffix cgo -o main .

# === Final stage: Create minimal runtime image ===
FROM <your-namespace>/dhi-golang:1-alpine3.21

WORKDIR /app
COPY --from=builder /app/main  /app/main

ENTRYPOINT ["/app/main"]
#syntax=docker/dockerfile:1

FROM <your-namespace>/dhi-golang:1-alpine3.21-dev

WORKDIR /app
ADD . ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-s -w" --installsuffix cgo -o main .

ENTRYPOINT ["/app/main"]

Node.js example

#syntax=docker/dockerfile:1

FROM node:latest
WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install

COPY image.jpg ./image.jpg
COPY . .

CMD ["node", "index.js"]
#syntax=docker/dockerfile:1

#=== Build stage: Install dependencies and build application ===#
FROM <your-namespace>/dhi-node:23-alpine3.21-dev AS builder
WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install

COPY image.jpg ./image.jpg
COPY . .

#=== Final stage: Create minimal runtime image ===#
FROM <your-namespace>/dhi-node:23-alpine3.21
ENV PATH=/app/node_modules/.bin:$PATH

COPY --from=builder --chown=node:node /usr/src/app /app

WORKDIR /app

CMD ["index.js"]
#syntax=docker/dockerfile:1

FROM <your-namespace>/dhi-node:23-alpine3.21-dev
WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install

COPY image.jpg ./image.jpg
COPY . .

CMD ["index.js"]

Python example

#syntax=docker/dockerfile:1

FROM python:latest AS builder

ENV LANG=C.UTF-8
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"

WORKDIR /app

RUN python -m venv /app/venv
COPY requirements.txt .

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

FROM python:latest

WORKDIR /app

ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"

COPY image.py image.png ./
COPY --from=builder /app/venv /app/venv

ENTRYPOINT [ "python", "/app/image.py" ]
#syntax=docker/dockerfile:1

#=== Build stage: Install dependencies and create virtual environment ===#
FROM <your-namespace>/dhi-python:3.13-alpine3.21-dev AS builder

ENV LANG=C.UTF-8
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"

WORKDIR /app

RUN python -m venv /app/venv
COPY requirements.txt .

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

#=== Final stage: Create minimal runtime image ===#
FROM <your-namespace>/dhi-python:3.13-alpine3.21

WORKDIR /app

ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"

COPY image.py image.png ./
COPY --from=builder /app/venv /app/venv

ENTRYPOINT [ "python", "/app/image.py" ]
#syntax=docker/dockerfile:1

FROM <your-namespace>/dhi-python:3.13-alpine3.21-dev

ENV LANG=C.UTF-8
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"

WORKDIR /app

RUN python -m venv /app/venv
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY image.py image.png ./

ENTRYPOINT [ "python", "/app/image.py" ]

Use Gordon

Alternatively, you can request assistance to Gordon, Docker's AI-powered assistant, to migrate your Dockerfile: