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.
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.
This passage assumes you are familiar with Golang,git and Docker, and know a little about GitHub Actions.
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
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
.
Using
go build
, you can build an executable binary for a Golang program.
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.
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.
With GitHub Actions, you can automate compiling, packaging and releasing binary executables to Release.
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.
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.
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.
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!
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.
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 }}
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
...