├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml ├── linters │ ├── .hadolint.yaml │ └── .markdown-lint.yml └── workflows │ └── linter.yml ├── Dockerfile ├── LICENSE ├── README.md └── example-tini └── Dockerfile /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: bretfisher 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | -------------------------------------------------------------------------------- /.github/linters/.hadolint.yaml: -------------------------------------------------------------------------------- 1 | ignored: 2 | - DL3003 #ignore that we use cd sometimes 3 | - DL3006 #image pin versions 4 | - DL3008 #apt pin versions 5 | - DL3018 #apk add pin versions 6 | - DL3022 #bad rule for COPY --from 7 | - DL3028 #gem install pin versions 8 | - DL3029 #--platform is correct, sometimes 9 | - DL3059 #multiple consecutive runs 10 | - DL4006 #we don't need pipefail in this 11 | - SC2016 #we want single quotes sometimes -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | # MD013/line-length - Line length 2 | MD013: 3 | # Number of characters 4 | line_length: 150 5 | # Number of characters for headings 6 | heading_line_length: 100 7 | code_blocks: false -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Code Base 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | call-super-linter: 12 | name: Lint Code Base 13 | # use Reusable Workflows to call my linter config remotely 14 | # https://docs.github.com/en/actions/learn-github-actions/reusing-workflows 15 | uses: bretfisher/super-linter-workflow/.github/workflows/super-linter.yaml@main -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | 3 | # this shows us what various BuildKit arguments are based on the 4 | # docker buildx build --platform= option you give Docker. 5 | 6 | # For best output results, build with --progress=plain --no-cache 7 | 8 | ARG TARGETPLATFORM 9 | ARG TARGETARCH 10 | ARG TARGETVARIANT 11 | RUN printf "NOTE: docker build --progress=plain --no-cache --platform=" 12 | RUN printf "TARGETPLATFORM=${TARGETPLATFORM}" 13 | RUN printf "TARGETARCH=${TARGETARCH}" 14 | RUN printf "TARGETVARIANT=${TARGETVARIANT}" 15 | RUN printf "With uname -s : " && uname -s 16 | RUN printf "and uname -m : " && uname -m 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker --platform translation example for TARGETPLATFORM 2 | 3 | Naming is hard. Having a consistent OS (kernel) and architecture naming scheme for building is harder. 4 | 5 | **Goal**: In Docker, our goal should be a single Dockerfile that can build for multiple Linux architectures. 6 | A stretch-goal might be cross-OS (Windows Containers), but for now let's focus on the Linux kernel. 7 | 8 | Turns out this might be harder then you're expecting. 9 | 10 | Docker has BuildKit which makes this **much easier** with the `docker buildx build --platform` option, and 11 | combined with the `ARG TARGETPLATFORM` gets us much closer to our goal. See the docs on 12 | [multi-platform building](https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images) 13 | and the [automatic platform ARGs](https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope). 14 | 15 | ## The problem with downloading binaries in Dockerfiles 16 | 17 | There are still inconsistencies we need to deal with. This problem rears its ugly head when you're 18 | trying to download pre-built binaries of various tools and dependencies (GitHub, etc.) that don't use 19 | a package manager (apt, yum, brew, apk, etc.). 20 | Download URLs are inconsistently named, and expect some sort of kernel and architecture combo in the file name. 21 | No one seems to agree on common file naming. 22 | 23 | Using `uname -m` won't work for all architectures, as the name changes based on where it's running. For example, with 24 | arm64 (v8) architecture, it might say arm64, or aarch64. In older arm devices it'll say armv71 even though you 25 | might want arm/v6. 26 | 27 | There's also the complexity that a device might have one architecture hardware (arm64) but run a different kernel (arm/v7 32-Bit). 28 | 29 | The containerd project has 30 | [created their own conversion table](https://github.com/containerd/containerd/blob/master/platforms/platforms.go#L88-L94), 31 | which I'm commenting on here. This is similar to (but not exactly) what `ARG TARGETPLATFORM` gives us: 32 | 33 | ```bash 34 | // Value Normalized 35 | // aarch64 arm64 # the latest v8 arm architecture. Used on Apple M1, AWS Graviton, and Raspberry Pi 3's and 4's 36 | // armhf arm # 32-bit v7 architecture. Used in Raspberry Pi 3 and Pi 4 when 32bit Raspbian Linux is used 37 | // armel arm/v6 # 32-bit v6 architecture. Used in Raspberry Pi 1, 2, and Zero 38 | // i386 386 # older Intel 32-Bit architecture, originally used in the 386 processor 39 | // x86_64 amd64 # all modern Intel-compatible x84 64-Bit architectures 40 | // x86-64 amd64 # same 41 | ``` 42 | 43 | So that's a start. But BuildKit seems to do additional conversion, as you'll see in the testing below. 44 | 45 | ## Recommended approach for curl and wget commands in multi-platform Dockerfiles 46 | 47 | If we wanted to have a single Dockerfile build across (at minimum) x86-64, ARM 64-Bit, and ARM 32-Bit, 48 | we can use BuildKit with the `TARGETPLATFORM` argument to get a more consistent environment variable in our 49 | `RUN` commands, but it's not perfect. We'll still need to convert that output to what our `RUN` commands need. 50 | 51 | `TARGETPLATFORM` is actually the combo of `TARGETOS`/`TARGETARCH`/`TARGETVARIANT` so in some cases you could use 52 | those to help the situation, but as you can see below, the arm/v6 vs arm/v7 vs arm/v8 output can make all this 53 | tricky. `TARGETARCH` is too general, and `TARGETVARIANT` may be blank (in the case of `arm64`). 54 | 55 | So when I use `docker buildx build --platform`, what do I see inside the BuildKit environment? 56 | 57 | Here's my results for this Dockerfile: 58 | 59 | ```Dockerfile 60 | FROM busybox 61 | ARG TARGETPLATFORM 62 | ARG TARGETARCH 63 | ARG TARGETVARIANT 64 | RUN printf "I'm building for TARGETPLATFORM=${TARGETPLATFORM}" \ 65 | && printf ", TARGETARCH=${TARGETARCH}" \ 66 | && printf ", TARGETVARIANT=${TARGETVARIANT} \n" \ 67 | && printf "With uname -s : " && uname -s \ 68 | && printf "and uname -m : " && uname -m 69 | ``` 70 | 71 | Here are the results when using the command `docker buildx build --progress=plain --platform= .`: 72 | 73 | 1. `--platform=linux/amd64` and `--platform=linux/x86-64` and `--platform=linux/x86_64` 74 | 75 | ```text 76 | I'm building for TARGETPLATFORM=linux/amd64, TARGETARCH=amd64, TARGETVARIANT= 77 | With uname -s : Linux 78 | and uname -m : x86_64 79 | ``` 80 | 81 | 2. `--platform=linux/arm64` and `--platform=linux/arm64/v8` **TARGETVARIANT is blank** 82 | 83 | ```text 84 | I'm building for TARGETPLATFORM=linux/arm64, TARGETARCH=arm64, TARGETVARIANT= 85 | With uname -s : Linux 86 | and uname -m : aarch64 87 | ``` 88 | 89 | 3. `--platform=linux/arm/v8` **Don't use this. It builds but is inconsistent.** 90 | I'd think this would be an alias to arm64, but it returns weird results (uname thinks it's 32bit, TARGETARCH is not arm64) 91 | 92 | ```text 93 | I'm building for TARGETPLATFORM=linux/arm/v8, TARGETARCH=arm, TARGETVARIANT=v8 94 | With uname -s : Linux 95 | and uname -m : armv7l 96 | ``` 97 | 98 | 4. `--platform=linux/arm` and `--platform=linux/arm/v7` and `--platform=linux/armhf` 99 | 100 | ```text 101 | I'm building for TARGETPLATFORM=linux/arm/v7, TARGETARCH=arm, TARGETVARIANT=v7 102 | With uname -s : Linux 103 | and uname -m : armv7l 104 | ``` 105 | 106 | 5. `--platform=linux/arm/v6` and `--platform=linux/armel` 107 | 108 | ```text 109 | I'm building for TARGETPLATFORM=linux/arm/v6, TARGETARCH=arm, TARGETVARIANT=v6 110 | With uname -s : Linux 111 | and uname -m : armv7l 112 | ``` 113 | 114 | 6. `--platform=linux/i386` and `--platform=linux/386` 115 | 116 | ```text 117 | I'm building for TARGETPLATFORM=linux/386, TARGETARCH=386, TARGETVARIANT= 118 | With uname -s : Linux 119 | and uname -m : i686 120 | ``` 121 | 122 | ## So what then, how do we proceed? 123 | 124 | ### Know what platforms you can build in your Docker Engine 125 | 126 | First, you'll need to know what platforms your Docker Engine can build. 127 | Docker can support multi-platform builds with the `buildx` command. 128 | The [README is great](https://github.com/docker/buildx#building-multi-platform-images). 129 | By default it only supports the platform that Docker Engine (daemon) is running on, but if QEMU is installed, it can emulate many others. 130 | You can see the list it's currently enabled for with the `docker buildx inspect --bootstrap` command. 131 | 132 | For example, this is what I see in Docker Desktop on a Intel-based Mac and a Windows 10 with WSL2, 133 | with `linux/amd64` being the native platform, and the rest using QEMU emulation: 134 | 135 | `linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6` 136 | 137 | I see the same list in Docker Desktop on a Apple M1 Mac, with `linux/arm64` being the native platform, and the 138 | rest using QEMU emulation: 139 | 140 | `linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6` 141 | 142 | This is what I see in Docker for Linux on a Raspberry Pi 4 with Raspbian (32bit as of early 2021). QEMU isn't 143 | enabled by default, so only the native options show up: 144 | 145 | `linux/arm/v7, linux/arm/v6` 146 | 147 | This is what I see in Docker for Linux on a Digital Ocean amd64 standard droplet. Notice again, 148 | QEMU isn't setup so the list is much shorter: 149 | 150 | `linux/amd64, linux/386` 151 | 152 | ### Add Dockerfile logic to detect the platform it needs to use 153 | 154 | Let's use [tini](https://github.com/krallin/tini) as an example of how to ensure that a single 155 | Dockerfile and download the correct tini build into our container image for Linux on amd64, arm64, arm/v7, arm/v6, and i386. 156 | We'll use a separate build-stage, evaluate the `TARGETPLATFORM`, and manually convert the value 157 | (via `sh case` statement) to what the specific binary URL needs. 158 | 159 | This was inspired by @crazy-max in his [docker-in-docker Dockerfile](https://github.com/crazy-max/docker-docker/blob/1b0a1260bdbcb5931e07b5bc21e7bb0991101fda/Dockerfile-20.10#L12-L18). 160 | 161 | See the full Dockerfile here: [example-tini\Dockerfile](example-tini\Dockerfile) 162 | 163 | ```Dockerfile 164 | FROM --platform=${BUILDPLATFORM} alpine as tini-binary 165 | ENV TINI_VERSION=v0.19.0 166 | ARG TARGETPLATFORM 167 | RUN case ${TARGETPLATFORM} in \ 168 | "linux/amd64") TINI_ARCH=amd64 ;; \ 169 | "linux/arm64") TINI_ARCH=arm64 ;; \ 170 | "linux/arm/v7") TINI_ARCH=armhf ;; \ 171 | "linux/arm/v6") TINI_ARCH=armel ;; \ 172 | "linux/386") TINI_ARCH=i386 ;; \ 173 | esac \ 174 | && wget -q https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TINI_ARCH} -O /tini \ 175 | && chmod +x /tini 176 | ``` 177 | 178 | ## Further Reading 179 | 180 | Docker Blog from Adrian Mouat on [multi-platform Docker builds](https://www.docker.com/blog/multi-platform-docker-builds/). 181 | 182 | ## **MORE TO COME, WIP** 183 | 184 | - [ ] Background on manifests, multi-architecture repos 185 | - [ ] Using third-party tools like `regctl` to make your life easier (i.e. `regctl image manifest --list golang`) 186 | - [ ] Breakdown the three parts of the platform ARG better 187 | -------------------------------------------------------------------------------- /example-tini/Dockerfile: -------------------------------------------------------------------------------- 1 | # example base image for your app 2 | FROM debian as base 3 | 4 | # default to the build platforms image, and not the target platform image 5 | # since this is a temp image stage, we should avoid qemu for the binary download 6 | # and only pull the alpine image once 7 | FROM --platform=${BUILDPLATFORM} alpine as tini-binary 8 | ENV TINI_VERSION=v0.19.0 9 | # Use BuildKit to help translate architecture names 10 | ARG TARGETPLATFORM 11 | # translating Docker's TARGETPLATFORM into tini download names 12 | RUN case ${TARGETPLATFORM} in \ 13 | "linux/amd64") TINI_ARCH=amd64 ;; \ 14 | "linux/arm64") TINI_ARCH=arm64 ;; \ 15 | "linux/arm/v7") TINI_ARCH=armhf ;; \ 16 | "linux/arm/v6") TINI_ARCH=armel ;; \ 17 | "linux/386") TINI_ARCH=i386 ;; \ 18 | esac \ 19 | && wget -q https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TINI_ARCH} -O /tini \ 20 | && chmod +x /tini 21 | 22 | 23 | 24 | # your final app image, copy tini into /usr/local/bin 25 | FROM base as release 26 | COPY --from=tini-binary /tini /usr/local/bin/tini 27 | ENTRYPOINT ["/tini", "--"] 28 | 29 | # Run your program under Tini 30 | CMD ["/your/program", "-and", "-its", "arguments"] 31 | 32 | --------------------------------------------------------------------------------