charnould
charnould

Reputation: 2907

How to build a docker image for M1 Mac Silicon or AMD conditionally from Dockerfile?

Some context:

For building my container for prod, being on a M1 Mac, I have the below Dockerfile:
See the --platform=linux/amd64 arg in FROM and It works (= I'm able to deploy).

FROM --platform=linux/amd64 python:3.10-slim-bullseye
WORKDIR /usr/src/app
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
ENV FLASK_ENV=development 
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY . .
CMD ["flask", "run"]

However, how can I say in my Dockerfile to not use --platform=linux/amd64 if I want to build for local development?
I've seen some post on SO with conditions in dockerfile but only for RUN command.
Any idea or best practices?
Thanks.

Upvotes: 11

Views: 23155

Answers (3)

EdH
EdH

Reputation: 3293

I spent a lot of time researching this myself - quite frustrated that builds on my Mac were taking so long.

  • You should not have two Dockerfiles, but rather a single multi-arch-capable Dockerfile.
  • Use tonistiigi/xx in a multi-stage build if you want fast build times and no arch-specific settings.
  • I'm using a Golang project as an example but it will apply to any Dockerfile.

Method 1 - use buildx QEMU

If you have a "regular" Dockerfile - i.e. you don't have a --platform statement in there or anything architecture-specific, you can probably just do:

docker buildx build -f Dockerfile --platform linux/amd64 --tag my-container:latest --load .

This will use QEMU - but it is slow and it sometimes breaks (compiler switches, etc). That's because the build itself is run in an emulator (in this case emulating amd64)

Method 2: Roll your own mulit-arch image:

If you had a Dockerfile like this:

FROM golang:1.20

ADD app /app
ENV CGO_ENABLED=0
RUN cd /app && go build -o app .
WORKDIR /app
ENTRYPOINT [ "/app/app" ]

Make it this:

FROM --platform=$BUILDPLATFORM golang:1.20 AS build
# get gcc cross toolchain
RUN DEBIAN_FRONTEND=noninteractive apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get -y install g++-x86-64-linux-gnu libc6-dev-amd64-cross
RUN DEBIAN_FRONTEND=noninteractive apt-get -y clean

ADD app /app
ARG TARGETOS TARGETARCH
ENV CGO_ENABLED=1
WORKDIR /app
# RUN gcc -dumpmachine with --progress=plain here to see what gcc is used by default
RUN CC=x86_64-linux-gnu-gcc GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o app .
ENTRYPOINT [ "/app/app" ]

Notice we have to install a specific cross toolchain for the architecture we need. This is not ideal, because it makes the Dockerfile arch-specific. :/

Method 3: Use the tonistiigi/xx tools Docker layer

Instead we can use this tonistiigi/xx package as a base layer. It provides tools which make cross builds way easier and still running native.

FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
FROM --platform=$BUILDPLATFORM golang:1.20

RUN DEBIAN_FRONTEND=noninteractive apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get -y install build-essential
RUN DEBIAN_FRONTEND=noninteractive apt-get -y clean

COPY --from=xx / /
ADD app /app
ARG TARGETPLATFORM
RUN xx-apt install -y libc6-dev gcc
ENV CGO_ENABLED=1
WORKDIR /app
RUN xx-go --wrap
RUN go build -o app . && xx-verify app
ENTRYPOINT [ "/app/app" ]

The xx- tools provide wrappers which understand the Docker multiplatform variables and then use the right cross toolchain to build the correct TARGETARCH architecture package.

This is the best way - it produce fast native, cross builds regardless of where you run the docker build buildx command. And it should work on Docker Desktop or docker installed on Linux as well - aarch64 or amd64 alike.

The same build command:

docker buildx build \
  -f Dockerfile \
  --platform linux/amd64 \
  --tag my-container:latest \
  --load .

is used for all three methods.

To the OP - this means they could run the command on Docker Desktop (MacOS side) or using docker installed in their Linux VM.

Check out my blog post:

https://www.izumanetworks.com/blog/build-docker-on-apple-m/

for more details.

Upvotes: 1

DBencz
DBencz

Reputation: 1309

I would either

  • not specify the platform in the Dockerfile, and build it the platform-specific way locally (or for production by speciying the build step in the CI) with this command for M1 mac: docker buildx build --platform=linux/arm64 -t myTag ., or
  • have two separate Dockerfiles (such es Dockerfile.intel, and Dockerfile), with the respective platform defined in them. This way, any build pipeline will pick Dockerfile as default, but for local build, you can specify which one to use, like so: docker build -f Dockerfile.intel .

To run your image as a dev conteiner, specify the image in a devcontainer.json, and start + attach by typing Reopen in Container in the command palette in VSCode.

Upvotes: 1

Loyen
Loyen

Reputation: 156

I believe you can use the --platform parameter on docker buildx build or docker build to set platform(s) to build the image which will be used within any FROM calls within the Dockerfile if nothing else is specified (see Dockerfile FROM), as mentioned in the documentation.

You can then use the TARGETPLATFORM variable within your Dockerfile to get what platform it's being built for if needed. If you want to change the default platform to build for, you can set the DOCKER_DEFAULT_PLATFORM environment variable.

Upvotes: 14

Related Questions