Build Go Containers with Ko
In this tutorial, you'll learn how to build minimal Go Containers using Ko and Chainguard base images
Chainguard’s Dockerfile Converter (dfc) was designed to facilitate the process of porting existing Dockerfiles to use Chainguard Containers. The following platforms are currently supported:
apk
)apt
, apt-get
)yum
, dnf
, microdnf
)If you use Homebrew, you can install dfc with:
brew install chainguard-dev/tap/dfc
If you prefer to use the Go toolchain, you can install dfc directly from source. The following command will download the latest version of dfc and install it in your $GOPATH/bin
directory:
go install github.com/chainguard-dev/dfc@latest
If that does not work, make sure the PATH environment variable is properly set:
export PATH="$PATH:$(go env GOPATH)/bin"
To verify that the installation was successful, run:
dfc -v
You will receive output indicating which version of dfc you have installed, such as:
dfc version v0.9.3
DFC supports inline conversion of entire Dockerfiles and single FROM
and RUN
instructions. It can also read Dockerfiles from files or standard input (stdin). The converted Dockerfile will use Chainguard Containers as the base image, which are available at cgr.dev/<org>
.
With inline usage, you can convert single instructions or entire Dockerfiles. For example, to convert a single FROM
line, you can run:
echo "FROM node" | dfc -
This will give you the following result:
FROM cgr.dev/ORG/node:latest
The ORG
here is a placeholder that you should replace with your unique organization’s namespace at Chainguard, or use chainguard
for the free tier of images. In the customizing section you’ll learn how to use the --org
flag to set this up automatically.
You can also convert single RUN
directives such as the following:
echo "RUN apt-get update && apt-get install -y nano" | dfc -
Which will give you the following output:
RUN apk add --no-cache nano
It is also possible to convert a whole Dockerfile using inline mode. Here we use a heredoc input stream to create the Dockerfile content and pipe it to dfc
:
cat <<DOCKERFILE | dfc -
FROM node
RUN apt-get update && apt-get install -y nano
DOCKERFILE
This will convert to:
FROM cgr.dev/ORG/node:latest-dev
USER root
RUN apk add --no-cache nano
Notice the USER root
directive that has been included here to allow for package installations. You’ll find details of why that happens in the How it Works section of this guide.
Unless specified, dfc will not make any direct changes to your Dockerfile, writing the results to the default output stream. To convert a Dockerfile and save the output to a new file called Dockerfile.converted
, run the following command:
dfc ./Dockerfile > ./Dockerfile.converted
You can also pipe the Dockerfile’s contents from stdin:
cat ./Dockerfile | dfc -
DFC was designed to work offline by default, which means it doesn’t validate image names or check for the existence of images in a live registry. Instead, it relies on a set of rules and mappings to convert Dockerfiles to use Chainguard Containers. This includes not only the change of base image used in a Dockerfile, but also the conversion of package managers and commands used in RUN
instructions.
DFC will convert the FROM
instruction in a Dockerfile using two main mechanisms:
dfc
programmatically rewrites the registry path in all FROM
instructions to align with the Chainguard registry format. By default, it inserts a placeholder for the organization name, which can be explicitly set using the --org
flag. This mechanism ensures that resulting images are sourced from the appropriate organization namespace within the Chainguard container registry.dfc
also performs automated translation of image names based on an internal mapping file. This mapping correlates common base images and their versions to their Chainguard Container equivalents. Custom mappings can be defined to accommodate project-specific requirements, as detailed in a subsequent section. When a mapping does not exist for a specific image, dfc
will default to use the same image name as the original Dockerfile.For example, the following command will convert an inline FROM
instruction using node
as the base image:
echo "FROM node" | dfc -
You will receive the following output:
FROM cgr.dev/ORG/node:latest
The tag conversion process follows a set of rules:
chainguard-base
, always use the latest
tag.
debian
, it will be converted to cgr.dev/ORG/chainguard-base:latest
.latest
.
FROM node
will be converted to FROM cgr.dev/ORG/node:latest
.FROM node:14.17.3
will be converted to FROM cgr.dev/ORG/node:14.17
.latest
, stable
, etc.), it will be converted to latest
.chainguard-base
, add -dev
suffix when there are RUN
commands.
FROM node:14
will be converted to FROM cgr.dev/ORG/node:14-dev
if the Dockerfile has RUN
instructions.RUN
commands, it will not have the -dev
suffix, e.g., FROM node:14
will be converted to FROM cgr.dev/ORG/node:14
.More Examples:
FROM node:14
→ FROM cgr.dev/ORG/node:14-dev
(if stage has RUN commands)FROM node:14.17.3
→ FROM cgr.dev/ORG/node:14.17-dev
(if stage has RUN commands)FROM debian:bullseye
→ FROM cgr.dev/ORG/chainguard-base:latest
(always)FROM golang:1.19-alpine
→ FROM cgr.dev/ORG/go:1.19-dev
(if stage has RUN commands)FROM node:${VERSION}
→ FROM cgr.dev/ORG/node:${VERSION}-dev
(if stage has RUN commands)When a Dockerfile uses package managers such as apt
, dnf
, or yum
to install software, dfc
automatically translates these commands to their apk
equivalents. This conversion leverages an internal mapping file to resolve package name differences between distributions, ensuring accurate and compatible package installation within the Chainguard Containers environment.
In addition to that, DFC automatically includes a USER root
directive when it detects package installation commands in the Dockerfile. This is necessary because Chainguard Containers are built with a non-root user by default, and package installation commands typically require root privileges.
For example, consider the following Dockerfile based on Fedora:
FROM fedora
RUN dnf -y update && dnf clean all && dnf -y install python-pip && dnf clean all
ADD . /src
RUN cd /src; pip install -r requirements.txt
EXPOSE 8080
CMD ["python", "/src/index.py"]
The following command will convert this Dockerfile and print the results to stdout:
dfc Dockerfile
You will receive the following output:
FROM cgr.dev/ORG/chainguard-base:latest
USER root
RUN apk add --no-cache python-pip
ADD . /src
RUN cd /src; pip install -r requirements.txt
EXPOSE 8080
CMD ["python", "/src/index.py"]
DFC will automatically change any useradd
and groupadd
instructions in your Dockerfile to adduser
and addgroup
to match Busybox’s syntax. This ensures that user and group creation commands execute correctly within the Chainguard Containers environment, maintaining consistent behavior and preventing build failures.
For example, consider the following Dockerfile that adds a nonroot
user to the image:
FROM php:8.3-cli
RUN apt-get update && apt-get install -y \
git \
curl \
libxml2-dev \
zip \
unzip
# Install Composer and set up application
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY . /app
# set up nonroot system user
RUN useradd -r -s /bin/bash nonroot && \
chown -R nonroot /app && \
cd /app && composer install
USER nonroot
ENTRYPOINT [ "php", "minicli", "mycommand" ]
When converting this Dockerfile with dfc
, the useradd
command will be replaced with adduser
, and the resulting Dockerfile will look like this:
FROM cgr.dev/ORG/php:8.3-dev
USER root
RUN apk add --no-cache curl git libxml2-dev unzip zip
# Install Composer and set up application
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY . /app
# set up nonroot system user
RUN adduser --system --shell /bin/bash nonroot && \
chown -R nonroot /app && \
cd /app && \
composer install
USER nonroot
ENTRYPOINT [ "php", "minicli", "mycommand" ]
Multi-stage builds are crucial for creating smaller, more secure, and efficient container images. By separating the build environment from the final runtime environment, they significantly reduce the final image size by discarding unnecessary build-time dependencies. This also enhances security by minimizing the attack surface.
DFC is designed to recognize and process multi-stage Dockerfiles the same way it handles regular single-stage Dockerfiles, converting them to leverage the advantages of Chainguard Containers.
Consider this multi-stage Python Dockerfile, which includes a few build-time dependencies and a leaner runtime:
FROM python:3.9 as builder
WORKDIR /app
RUN apt update && apt install -y curl git
ENV PATH="/venv/bin:$PATH"
RUN python -m venv /app/venv
COPY requirements.txt /app
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.9-slim
WORKDIR /app
ENV PATH="/venv/bin:$PATH"
COPY main.py /app
COPY --from=builder /app/venv /venv
CMD ["python", "/app/main.py"]
Running DFC on this Dockerfile with default options will give you the following result:
FROM cgr.dev/ORG/python:3.9-dev AS builder
USER root
WORKDIR /app
RUN apk add --no-cache curl git
ENV PATH="/venv/bin:$PATH"
RUN python -m venv /app/venv
COPY requirements.txt /app
RUN pip install --no-cache-dir -r requirements.txt
FROM cgr.dev/ORG/python:3.9
WORKDIR /app
ENV PATH="/venv/bin:$PATH"
COPY main.py /app
COPY --from=builder /app/venv /venv
CMD ["python", "/app/main.py"]
As you will notice, DFC used a distroless Python Chainguard Container for the final runtime stage: cgr.dev/ORG/python:3.9
. That is possible because DFC didn’t detect any RUN
instruction in the final stage. If we had system-level dependencies installed via apk
or other RUN
instructions in the runtime stage, DFC would instead default to the regular, fully-featured Python image from Chainguard Containers, which in this case would be cgr.dev/ORG/python:3.9-dev
.
For more details on distroless and how to work with multi-stage builds, check our guide on Getting Started with Distroless.
There are several ways to customize the conversion process of Dockerfiles using dfc
.
By default, dfc
uses ORG
as a placeholder for the image registry address. You can provide the --org
parameter to specify the organization that you’re a member of. To use free tier images, use chainguard
as the organization.
For example, the following command will convert a Dockerfile and set the organization to chainguard
:
echo "FROM node" | dfc --org chainguard -
You will receive output similar to this:
FROM cgr.dev/chainguard/node:latest
You can also specify a custom registry to use for the conversion. This is useful if you have your own registry. To do this, use the --registry
flag followed by the desired registry URL. For example, if you want to use myregistry.example.com
as the registry, you can run:
echo "FROM node" | dfc --registry myregistry.example.com -
You’ll get output like this:
FROM myregistry.example.com/node:latest
One thing to note is that the --registry
flag will override the --org
flag, so if you specify both, the --registry
will take precedence.
You can also provide a custom mapping file to dfc
using the --mapping
flag. This allows you to define your own rules for converting Dockerfiles, such as specific image names or package names that should be translated differently.
For example, consider the following Dockerfile based on the php:fpm
image:
FROM php:fpm
RUN apt-get update && apt-get install -y \
git \
curl \
libxml2-dev \
zip \
unzip
# Install Composer and set up application
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN mkdir /application
COPY . /application/
RUN cd /application && composer install
By default, DFC will convert the php:fpm
base image to Chainguard’s main PHP image, php:latest-dev
. However, we’d like this to be converted to php:latest-fpm-dev
, since that is the FPM variant of the PHP image in the Chainguard registry. To handle that case, we can create a custom mapping file called mappings.yaml
with the following content:
images:
php:fpm: php:latest-fpm-dev
Then, when running dfc
, we can specify this mapping file using the --mapping
flag:
dfc Dockerfile --mappings mappings.yaml
And this will produce the following output:
FROM cgr.dev/ORG/php:latest-fpm-dev
USER root
RUN apk add --no-cache curl git libxml2-dev unzip zip
# Install Composer and set up application
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN mkdir /application
COPY . /application/
RUN cd /application && composer install
By default, dfc will print the converted Dockerfile to stdout, and won’t make any changes to your original Dockerfile. You can use the --in-place
flag to make dfc overwrite the original file. This will also create a .bak
file to back up the original file contents.
dfc Dockerfile --in-place
2025/03/14 14:54:21 saving dockerfile backup to Dockerfile.bak
2025/03/14 14:54:21 overwriting Dockerfile
This method does not work with inline input.
If you plan on using dfc
programmatically, the JSON output can come in handy. For example, the following will convert an inline Dockerfile and output the results in JSON format, parsed by jq
for readability:
cat <<DOCKERFILE | dfc --org chainguard.edu --json - | jq
FROM node
RUN apt-get update && apt-get install -y nano
DOCKERFILE
{
"lines": [
{
"raw": "FROM node",
"converted": "FROM cgr.dev/chainguard.edu/node:latest-dev\nUSER root",
"stage": 1,
"from": {
"base": "node"
}
},
{
"raw": "RUN apt-get update && apt-get install -y nano",
"converted": "RUN apk add -U nano",
"stage": 1,
"run": {
"distro": "debian",
"manager": "apt-get",
"packages": [
"nano"
]
}
},
{
"raw": ""
}
]
}
Check also the Useful jq formulas section from the dfc repository as reference on how to use jq to filter the JSON output.
You can import the package github.com/chainguard-dev/dfc/pkg/dfc
and use it directly from Go code to parse and convert Dockerfiles on your own, without the dfc CLI. This way, you can integrate dfc into your own Go applications or scripts, which can be especially useful if you have a large number of Dockerfiles to convert or if you want to further customize the output produced by dfc.
Check the Using from Go section on the dfc repository for examples on how to use it as a Go library.
While dfc operates completely offline and does not in itself use AI to perform conversion of Dockerfiles, it can be leveraged as an MCP (Model Context Protocol) Server to integrate with an AI-based prompt engineering workflow.
This experimental capability allows AI agents to utilize DFC’s powerful conversion logic within a broader AI-driven engineering ecosystem. It’s particularly useful for scenarios where automation handles 90% of the conversion and AI manages edge cases and custom logic.
For more information on how to use DFC with AI agents, check the Project’s README on GitHub.
If you’d like to learn more about our Dockerfile Converter, including how to get involved with the project, you can check out the dfc repository on GitHub. We welcome contributions and feedback from the community.
Last updated: 2025-04-30 15:22