This post is automatically translated with LLM. The translation content has NOT been reviewed and may contain errors.
Since I have multiple devices with different architectures running Docker (including x86_64 computers and servers, ARM32v7 Tinker Board, and ARM64v8 Raspberry Pi 3B), each of my Docker images needs to be built in multiple versions. Initially, I wrote a separate Dockerfile for each architecture, but this approach proved difficult to manage uniformly, often leading to missed updates when modifying Dockerfiles during software upgrades. Later, I adopted Docker's build argument feature, using the --build-arg
parameter to select different base images and download architecture-specific files based on arguments.
However, this approach still has significant limitations. First, different projects use varying naming conventions for architectures. For example, the x86 32-bit architecture (i386) is called "386" in Go and many Go-based projects. Similarly, ARM32v7 may be referred to as ARMHF, while ARM64v8 has three common variants (ARM64v8, ARM64, AARCH64). Previously, I had to use bash scripts to convert between these naming conventions for different contexts, requiring numerous variables and complicating the process.
Additionally, some images require architecture-specific handling. For instance:
- The nginx image cannot use Cloudflare-optimized zlib on i386 or ARM32v7 and must use the vanilla version.
- Compiling nginx for i386 on x86_64 requires
setarch i386
to avoid generating 64-bit binaries. - OpenLiteSpeed's official packages don't support ARM architectures, causing ARM image builds to fail—making it preferable to fail immediately rather than during the build process.
Moreover, many images share repetitive sections, such as initializing build parameters and invoking base images.
Dockerfile's native functionality is too simplistic—some of these requirements are cumbersome to implement, while others are outright impossible. Having #include
, #define
, and similar preprocessor directives in Dockerfiles, like in C++, would greatly simplify the process.
In this Docker Issue, commenter jfinkhaeuser suggested using m4 or cpp for Dockerfile preprocessing. Note: "cpp" here refers to the C preprocessor (not C++), available in systems with gcc. These preprocessors can handle any text files.
During testing, I encountered issues: For example, cpp treated /*
in rm -rf /tmp/*
as a comment start, deleting subsequent content. Adding backslashes would preserve them in the output, affecting execution; adding spaces (like / *
) is also problematic—as demonstrated by the Bumblebee incident. Similarly, https://www.example.com
lost content after //
. m4's syntax was too different and complex for my needs.
Fortunately, alternatives exist. I ultimately chose GPP (Generic Preprocessor) because its syntax is highly configurable. Unlike cpp, I can disable comment processing to preserve critical content like URLs and paths.
Installing GPP
On Debian:
apt-get install gpp
On Arch Linux (via AUR):
pikaur -S gpp
(Why isn't it in Arch's main repository...)
Using GPP
Assume I have a template Dockerfile with #include
directives named template.Dockerfile
, and included files reside in another directory. Generate the complete Dockerfile with:
gpp -I /path/to/include --nostdinc -U "" "" "(" "," ")" "(" ")" "#" "" -M "#" "\n" " " " " "\n" "(" ")" +c "\\\n" "" -o Dockerfile /path/to/template.Dockerfile
This produces a Dockerfile ready for docker build
.
The custom parameters enable:
#define
macros (e.g.,#define WGET(url) wget --no-check-certificate -q url
)- Backslash line continuation handling
#if
/#else
logic similar to cpp
For advanced usage, consult GPP's documentation.
Beyond the lengthy parameters, GPP functions like cpp. For example, -D ARCH_ARM64V8
defines variables for conditional processing in Dockerfiles.
Now I can write architecture-specific logic:
#if defined(ARCH_AMD64)
FROM multiarch/alpine:amd64-edge
#elif defined(ARCH_I386)
FROM multiarch/alpine:i386-edge
#elif defined(ARCH_ARM32V7)
FROM multiarch/alpine:armhf-edge
#elif defined(ARCH_ARM64V8)
FROM multiarch/alpine:arm64-edge
#else
#error "Architecture not set"
#endif
Or execute architecture-dependent commands:
#ifdef ARCH_I386
RUN setarch i386 make -j4 \
&& setarch i386 make install
#else
RUN make -j4 \
&& make install
#endif
To streamline usage, I created a Makefile for generating Dockerfiles, building images, and pushing to Docker Hub (view here).
This approach significantly extends Dockerfile capabilities, enabling more flexible workflows and simplifying maintenance.
My Dockerfiles are available on GitHub as always.