dockerfile converter
CLI to convert Dockerfiles to use Chainguard Images and APKs in FROM and RUN lines etc.
You can install dfc from Homebrew:
brew install chainguard-dev/tap/dfcYou can also install dfc from source:
go install github.com/chainguard-dev/dfc@latestYou can also use the dfc container image (from Docker Hub or cgr.dev):
docker run --rm -v "$PWD":/work chainguard/dfc
docker run --rm -v "$PWD":/work cgr.dev/chainguard/dfcConvert Dockerfile and print converted contents to terminal:
dfc ./DockerfileSave the output to new Dockerfile called Dockerfile.chainguard:
dfc ./Dockerfile > ./Dockerfile.chainguardYou can also pipe from stdin:
cat ./Dockerfile | dfc -Convert the file in-place using --in-place / -i (saves backup in .bak file):
dfc --in-place ./Dockerfile
mv ./Dockerfile.bak ./Dockerfile # revertNote: the Dockerfile and Dockerfile.chainguard in the root of this repo are not actually for building dfc, they
are symlinks to files in the testdata/ folder so users can run the commands in this README.
By default, FROM lines that have been mapped to Chainguard images will use "ORG" as a placeholder under cgr.dev:
FROM cgr.dev/ORG/<image>To configure your cgr.dev namespace use the --org flag:
dfc --org="example.com" ./Dockerfile
Resulting in:
FROM cgr.dev/example.com/<image>If mistakenly ran dfc with no configuration options and just want to replace the ORG
in the converted file, you can run something like this:
sed "s|/ORG/|/example.com/|" ./Dockerfile > dfc.tmp && mv dfc.tmp ./DockerfileTo use an alternative registry domain and root namespace, use the --registry flag:
dfc --registry="r.example.com/cgr-mirror" ./Dockerfile
Resulting in:
FROM r.example.com/cgr-mirror/<image>Note: the --registry flag takes precedence over the --org flag.
If you need to use a modified version of the default, embedded mappings
file mappings.yaml, use the --mappings flag:
dfc --mappings="./custom-mappings.yaml" ./DockerfileWant to submit an update to the default mappings.yaml?
Please open a GitHub pull request.
For complete before and after examples, see the testdata/ folder.
echo "FROM node" | dfc -Result:
FROM cgr.dev/ORG/node:latest-devecho "RUN apt-get update && apt-get install -y nano" | dfc -Result:
RUN apk add --no-cache nanocat <<DOCKERFILE | dfc -
FROM node
RUN apt-get update && apt-get install -y nano
DOCKERFILEResult:
FROM cgr.dev/ORG/node:latest-dev
USER root
RUN apk add --no-cache nanodfc detects the package manager being used and maps this to
a supported distro in order to properly convert RUN lines.
The following platforms are recognized:
| OS | Package manager |
|---|---|
| Alpine ("alpine") | apk |
| Debian/Ubuntu ("debian") | apt-get / apt |
| Fedora/RedHat/UBI ("fedora") | yum / dnf / microdnf |
For each FROM line in the Dockerfile, dfc attempts to replace the base image with an equivalent Chainguard Image.
For each RUN line in the Dockerfile, dfc attempts to detect the use of a known package manager (e.g. apt-get / yum / apk), extract the names of any packages being installed, try to map them via the package mappings in mappings.yaml, and replacing the old install with apk add --no-cache <packages>.
If dfc has detected the use of a package manager and ended up converting a RUN line,
then USER root will be appended under the last FROM line.
In the future we plan to handle this more elegantly, but this is the current state.
For each ARG line in the Dockerfile, dfc checks if the ARG is used as a base image in a subsequent FROM line. If it is, and the ARG has a default value that appears to be a base image, then dfc will modify the default value to use a Chainguard Image instead.
Since adding users and groups in Chainguard Images in Dockerfiles requires
adduser / addgroup (via busybox), when we detect the use of
useradd or groupadd commands in RUN lines, we will automatically try to
convert them to the equivalent adduser / addgroup commands.
If we see that you have installed the shadow package
(which actually provides useradd and groupadd), then we do not modify
these commands and leave them as is.
The syntax for the tar command is slightly different in busybox than it is
in the GNU version which is present by default on various distros.
For that reason, we will attempt to convert tar commands in RUN lines
using the GNU syntax to use the busybox syntax instead.
When converting Dockerfiles, dfc applies the following logic to determine which Chainguard Image and tag to use:
- Image mappings are defined in the
mappings.yamlfile under theimagessection - Each mapping defines a source image name (e.g.,
ubuntu,nodejs) and its Chainguard equivalent - Glob matching is supported using the asterisk (*) wildcard (e.g.,
nodejs*matches bothnodejsandnodejs20-debian12) - If a mapping includes a tag (e.g.,
chainguard-base:latest), that tag is always used - If no tag is specified in the mapping (e.g.,
node), tag selection follows the standard tag mapping rules - If no mapping is found for a base image, the original name is preserved and tag mapping rules apply
The tag conversion follows these rules:
-
For chainguard-base:
- Always uses
latesttag, regardless of the original tag or presence of RUN commands
- Always uses
-
For tags containing ARG variables (like
${NODE_VERSION}):- Preserves the original variable reference
- Adds
-devsuffix only if the stage contains RUN commands - Example:
FROM node:${NODE_VERSION}→FROM cgr.dev/ORG/node:${NODE_VERSION}-dev(if stage has RUN commands)
-
For other images:
- If no tag is specified in the original Dockerfile:
- Uses
latest-devif the stage contains RUN commands - Uses
latestif the stage has no RUN commands
- Uses
- If a tag is specified:
- If it's a semantic version (e.g.,
1.2.3orv1.2.3):- Truncates to major.minor only (e.g.,
1.2) - Adds
-devsuffix only if the stage contains RUN commands
- Truncates to major.minor only (e.g.,
- If the tag starts with
vfollowed by numbers, thevis removed - For non-semver tags (e.g.,
alpine,slim):- Uses
latest-devif the stage has RUN commands - Uses
latestif the stage has no RUN commands
- Uses
- If it's a semantic version (e.g.,
- If no tag is specified in the original Dockerfile:
This approach ensures that:
- Development variants (
-dev) with shell access are only used when needed - Semantic version tags are simplified to major.minor for better compatibility
- The final stage in multi-stage builds uses minimal images without dev tools when possible
- Build arg variables in tags are preserved with proper
-devsuffix handling
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(if stage has RUN commands)FROM node:${VERSION}→FROM cgr.dev/ORG/node:${VERSION}-dev(if stage has RUN commands)
Get converted Dockerfile as JSON using --json / -j:
dfc --json ./DockerfilePipe it to jq:
dfc -j ./Dockerfile | jqReconstruct the Dockerfile pre-conversion:
dfc -j ./Dockerfile | jq -r '.lines[]|(.extra + .raw)'Reconstruct the Dockerfile post-conversion:
dfc -j ./Dockerfile | jq -r '.lines[]|(.extra + (if .converted then .converted else .raw end))'Convert and strip comments:
dfc -j ./Dockerfile | jq -r '.lines[]|(if .converted then .converted else .raw end)'Get list of all distros detected from RUN lines:
dfc -j ./Dockerfile | jq -r '.lines[].run.distro' | grep -v null | sort -uGet list of package managers detected from RUN lines:
dfc -j ./Dockerfile | jq -r '.lines[].run.manager' | grep -v null | sort -uGet all the packages initially detected during parsing:
dfc -j ./Dockerfile | jq -r '.lines[].run.packages' | grep '"' | cut -d'"' -f 2 | sort -u | xargsThe package github.com/chainguard-dev/dfc/pkg/dfc can be imported in Go and you can
parse and convert Dockerfiles on your own without the dfc CLI:
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/chainguard-dev/dfc/pkg/dfc"
)
var (
raw = []byte(strings.TrimSpace(`
FROM node
RUN apt-get update && apt-get install -y nano
`))
org = "example.com"
)
func main() {
ctx := context.Background()
// Parse the Dockefile bytes
dockerfile, err := dfc.ParseDockerfile(ctx, raw)
if err != nil {
log.Fatalf("ParseDockerfile(): %v", err)
}
// Convert
converted, err := dockerfile.Convert(ctx, dfc.Options{
Organization: org,
})
if err != nil {
log.Fatalf("dockerfile.Convert(): %v", err)
}
// Print converted Dockerfile content
fmt.Println(converted)
}- Incomplete Conversion: The tool makes a best effort to convert Dockerfiles but does not guarantee that the converted Dockerfiles will be buildable by Docker.
- Comment and Spacing Preservation: While the tool attempts to preserve comments and spacing, there may be cases where formatting is altered during conversion.
- Dynamic Variables: The tool may not handle dynamic variables in Dockerfiles correctly, especially if they are used in complex expressions.
- Unsupported Directives: Some Dockerfile directives may not be fully supported or converted, leading to potential build issues.
- Package Manager Commands: The tool focuses on converting package manager commands but may not cover all possible variations or custom commands.
- Multi-stage Builds: While the tool supports multi-stage builds, it may not handle all edge cases, particularly with complex stage dependencies.
- Platform-Specific Features: The tool may not account for platform-specific features or optimizations in Dockerfiles.
- Security Considerations: The tool does not perform security checks on the converted Dockerfiles, and users should review the output for potential vulnerabilities.
For issues related strictly to dfc as an open source tool,
please open a GitHub issue.
Chainguard customers: please share issues or feature requests with your support contact so we can prioritize and escalate internally (with or without a GitHub issue/PR).
Interested in Chainguard Images and want to get in touch with sales? Use this form.
