Skip to content

A demo on how to build go multiplatform binaries and publish to release and DockerHub with GitHub Actions.

License

Notifications You must be signed in to change notification settings

LeslieLeung/go-multiplatform-docker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-multiplatform-docker

build

English | 简体中文

For a updated version that uses GoReleaser, you should checkout LeslieLeung/gin-application-template.

A demo on how to build go multiplatform binaries and publish to release and DockerHub with GitHub Actions.

Why this exists?

As a software developer, it is not necessary to spend a lot of time repeating the same labor. This should be a high-level automation process. In the process of releasing software, there are some points that could be a pain in the ass:

  • build binaries for multiple OSs and architectures
  • might have to build a suitable build environment for cross-platform compiling
  • complicated release procedures

Sure, some of these inconveniences have been eliminated

  • Golang supports cross-platform compiling out-of-the-box
  • use Docker or VM
  • use scripts

However, it's not "automatic" enough. Using GitHub Actions, it can gracefully solve these problems, making developers more focus on the actual development.

Prerequisites

This passage assumes you are familiar with Golang,git and Docker, and know a little about GitHub Actions.

Let's begin!

There are two goals in this passage

  • Build multiplatform binaries for a Golang Program and release it to GitHub Releases
  • Build multiplatform binaries for a Golang Program and release it to DockerHub

Writing a simple Golang program

The Golang program is just for testing on different OSs and architectures, so a very simple program should do the trick. We will use a Hello World here.

package main

import "fmt"

func main() {
	fmt.Println("Hello, World!")
}

Run it in the terminal, and you will see the following.

> go run main.go
Hello, World!

Looking good, now let's build an executable binary.

> go build -o hello main.go

This command outputs nothing, means it ran successfully without any errors. In the world of command lines, no news is good news.

We shall see a hello executable file under current directory(In Windows, it might look like hello.exe)。Let's run it.

> ./hello
Hello, World!

Great, it has the same result as go run main.go.

Takeaways:

Using go build, you can build an executable binary for a Golang program.

Building multiplatform binary executables

Remember that on Windows, go build would produce a .exe file? It's worth mentioning that Golang's support for multiplatform compiling ability. It can produce binary executables for different platforms without any extra efforts.

Assuming you are using macOS for development, you can build a binary executable for Windows with the following command

> CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o hello_windows_amd64.exe main.go

There would be a hello_windows_amd64.exe file under current directory, copy it to a Windows machine a run it.

Build multiplatform binaries for a Golang Program and release it to GitHub Releases

Before you start, you might have a look at this repo's Releases

You might find for each target platform, there is a corresponding tar.gz or zip file with a md5 checksum. Inside the compressed file, there is a binary executable file, LISENCE and README.md.

This is quite simple with GitHub Actions, an action would do all the trick. See wangyoucao577/go-release-action .

It's really easy to use.

name: build

on:
  release:
    types: [created] # trigger when a release is created

jobs:
  build-go-binary:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        goos: [linux, windows, darwin] # the required OSs
        goarch: [amd64, arm64] # the required architectures
        exclude: # exclude some OSs and architectures
          - goarch: arm64
            goos: windows
    steps:
      - uses: actions/checkout@v3
      - uses: wangyoucao577/go-release-action@v1.30
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }} # a pre-defined secret to add files to Release
          goos: ${{ matrix.goos }}
          goarch: ${{ matrix.goarch }}
          goversion: 1.18 # the required Go version
          binary_name: "hello" # the name of the binary executable
          extra_files: LICENSE README.md # the extra files to add to the release

When you finish writing a version and ready to release, all you have to do is to tag the commit, say v0.0.2, the push it to GitHub. In Releases page, click Draft a new release, choose the tag, then click on Publish release in the bottom.

Then we can dive into Actions page, we should see a workflow running. When it's done, go back to Releases , and there is your release files.

Takeaway:

With GitHub Actions, you can automate compiling, packaging and releasing binary executables to Release.

Build multiplatform binaries for a Golang Program and release it to DockerHub

As mentioned before, the easiest way to run a Golang program is to compile it to a binary executable file, then run it. This doesn't even need a Golang environment on the actual machine.

Therefore, if you are to pack a Golang program into a Docker image, you only need a minimal Linux system and copy the binary executable into it.

With that in mind, we should be able to write this Dockerfile.

FROM alpine:3.15.5
COPY hello /usr/local/bin/hello
RUN chmod +x /usr/local/bin/hello

We should validate our idea. (Notice: The following code only works on Linux. On macOS and Windows it would not run for the compiled binary doesn't match the Docker image's target system and architecture.)

> docker build -t hello .
...(a lot of logs)
> docker run -it --rm hello
> hello
Hello, world!

There it goes! (Or you might fail if you didn't bother to look at the notice before.)

Success or not, you should learn that we should just compile binary executables for different platforms, and then we can build the corresponding Docker images.

Makefile Magic

As is mentioned earlier, a go build with arguments can produce binary executables for different platforms. But we might have various platforms and architectures, that's where Makefile comes into play.

Let's write a simple Makefile assigning different commands to build different platforms. Considering that we only need linux/amd64 and linux/arm64 for our Docker image, we only need the following lines. If you need a makefile that can run locally and build all the possible platforms, you can refer to the Makefile in this repo.

all: build-linux-amd64 build-linux-arm64

build-linux-amd64:
	mkdir -p build
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/hello_linux_amd64 main.go

build-linux-arm64:
	mkdir -p build
	CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o build/hello_linux_arm64 main.go

It's worth mentioning that the fist rule all is the default rule. When you run make, it will run the targets it assigned. mkdir -p build ensures that the output directory exists.

If you use macOS or Linux, you can try running make, it would create a build directory and put the binary executables into it.

Environment Variables in Dockerfile

If you failed in the former test, it is because that the wrong binary executable was packaged into the docker file. Therefore, our dockerfile should know which version of binary executable it should grab.

You might find in the previous Makefile, I used different suffixes for different platforms and architectures, like hello_linux_amd64. Now, we should make Dockerfile grab the correct binary executable judging from its target platform and architecture.

Let's modify the former Dockerfile to make it look like this.

FROM alpine:3.15.5
ARG TARGETOS
ARG TARGETARCH
COPY build/hello_${TARGETOS}_${TARGETARCH} /usr/local/bin/hello
RUN chmod +x /usr/local/bin/hello

TARGETOS and TARGETARCH is Automatic platform ARGs, but you have to use the ARG command to claim that you need them. With COPY, we copied the corresponding binary executable to /usr/local/bin and gave it executes permission. So user can run our program with hello.

If you failed in the former test, you might try it now.

Writing GitHub Actions

To this point, we've cleared all the hurdles, but we still have to write a GitHub Actions workflow to automate the process.

There is no need to create a new Action, we can just add a job to the former one.

build-docker-image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: docker/metadata-action@v4
        id: meta
        with:
          images: leslieleung/hello
      - uses: actions/setup-go@v3
        with:
          go-version: 1.18
      - uses: docker/setup-qemu-action@v2
      - uses: docker/setup-buildx-action@v2
      - uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }} # REMEMBER to add the secret in secrets
          password: ${{ secrets.DOCKERHUB_PASSWORD }}
      - run: make
      - uses: docker/build-push-action@v3
        with:
          context: .
          platforms: linux/arm64,linux/amd64 # required platforms
          push: true
          tags: ${{ steps.meta.outputs.tags }}

Release a version as mentioned before, then you can see your images on Dockerhub. You shall see linux/amd64 and linux/arm64.

There you go, well done!

Other good stuffs

According to the replies on my post on v2ex (see 关于 Golang 多平台打包发布这件事.. ), there are two other solutions.

  • GoReleaser : Provides the ability to compile cross-platform and build Docker images. Super cool tool with a free and a paid Pro version.
  • gox : Provides the ability to compile cross-platform in parallel.

gox can provide us a parallel compile, so let's look into it here.

Accelerate your release with gox

Upon checking out gox's documentation, it's quite easy-to-use. Let's add the following lines to the Makefile.

gox-linux:
	gox -osarch="linux/amd64 linux/arm64" -output="build/hello_{{.OS}}_{{.Arch}}"

gox-all:
	gox -osarch="darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64" -output="build/hello_{{.OS}}_{{.Arch}}"

Now, run make gox-linux or make gox-all should do all the magic.

Also, we have to modify our build.yml.

build-docker-image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: docker/metadata-action@v4
        id: meta
        with:
          images: leslieleung/hello
      - uses: actions/setup-go@v3
        with:
          go-version: 1.18
      - uses: docker/setup-qemu-action@v2
      - uses: docker/setup-buildx-action@v2
      - uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}
      - run: go install github.com/mitchellh/gox@latest # setup gox
      - run: make gox-linux
      - uses: docker/build-push-action@v3
        with:
          context: .
          platforms: linux/arm64,linux/amd64
          push: true
          tags: ${{ steps.meta.outputs.tags }}

FAQ

Failed to publish to Release due to permission issue

See link

Solution: add the following to build.yml .

name: build

on:
  release:
    types: [created]
    
permissions: # ADD ME
  contents: write # ADD ME

jobs:
  build-go-binary:
    runs-on: ubuntu-latest
...

References

GitHub Action - Build and push Docker images

GNU make

Dockerfile reference

wangyoucao577/go-release-action