From Apple Silicon to Heroku Docker Registry Without Swearing

From Apple Silicon to Heroku Docker Registry Without Swearing

Build Intel64-compatible Docker images from Mac M1

Docker Desktop (natively) and Heroku CLI (with Rosetta) work nicely on the Apple M1 processor, however, a locally built Docker image will not successfully deploy onto the Heroku Docker Registry.

In a previous article Deploy to Heroku From a MacBook M1: Heroku CLI or GitHubActions, I described how it is possible (and very convenient) to build a Docker image and deploy it to Heroku using GitHub Actions. Here I am going to look at how to, in a Mac development environment, build and release an image that is compatible with the Heroku Docker Registry runtime.

Image by author


What is happening?

Building and deploying Docker containers on Heroku Docker Registry is pretty simple. First, satisfy the prerequisites :

  • Login into Heroku (heroku login) and Heroku Docker Registry (heroku:container login)

  • Place the Dockerfile in the root folder

Build and release the application with a few simple steps:

# create Heroku app
% heroku create myapp
# set ConfigVar (if env variables are necessary)
% heroku config:set PARAM="value"
# build and push Docker image
% heroku container:push web -a myapp
# release
% heroku container:release web -a myapp

That should all be enough: the “push” and “release” phases of the build complete successfully, however when accessing the application (myapp.herokuapp.com) the “Error: Exec format error” appears.

Whaaat daa..

Screenshot of Heroku logs showing ‘Error” Exec format error

Screenshot by author

Let’s look into the image created by the Heroku command above

% docker inspect registry.heroku.com/myapp/web
[
    {
        "Id": "sha256:38005888ba52553018768ee5f4997d4d599fbeafed88c166f2c0a71fc6b49abc",
        "RepoTags": [
            "registry.heroku.com/myapp/web:latest"
        ],
...
   },
   "Architecture": "arm64",
   "Os": "linux",

Docker is by design multi-platform and can run on different architectures, however, the images must match the platform they will be run on. Which is not our case.

Option A: buildx

Buildx is a Docker plugin that allows, amongst other features, to build multi-platform images.

We are developing on the Mac ARM architecture but we want to create a x86-compatible image. The solution is NOT to use the heroku:container push command but rather to build the image locally with Docker buildx.

docker buildx build --platform linux/amd64 -t myapp .
# tag image to match Heroku naming conventions
docker tag myapp registry.heroku.com/myapp/web
# push
docker push registry.heroku.com/myapp/web

Once pushed the image can be released

heroku container:release web -a myapp

Option B: set DOCKER_DEFAULT_PLATFORM

The DOCKER_DEFAULT_PLATFORM environment variable permits to set the default platform for the commands that take the --platform flag.

export DOCKER_DEFAULT_PLATFORM=linux/amd64

You can now go ahead and use the standard Heroku CLI commands (since Docker underneath will run with the platform indicated by the setting above).

# build and push Docker image
% heroku container:push web -a myapp
# release
% heroku container:release web -a myapp

Conclusion

The two solutions presented are both valid: my personal choice is to “delegate” the build to a CI/CD tool (like GitHub Actions), or to settle for the first option (buildx) which keeps the flexibility of switching easily between platforms (it may not the case when using an environment variable).

Check out the GitHub repository showing various options to deploy on Heroku.

Happy coding ✌️