Golang cross compiling with CGO inside docker image - docker

Requirement: An application has to be containerised as a docker image and needs to support arm64 and amd64 architectures.
Codebase: It is a golang application that needs to make use of git2go library and must have CGO_ENABLED=1 to build the project. The minimum reproducible example can be found here on github.
Host machine: I am using arm64 M1 mac and docker desktop to build the app but the results are similar on our amd64 Jenkins CI build system.
Dockerfile:
FROM golang:1.17.6-alpine3.15 as builder
WORKDIR /workspace
COPY go.mod go.mod
COPY go.sum go.sum
RUN apk add --no-cache libgit2 libgit2-dev git gcc g++ pkgconfig
RUN go mod download
COPY main.go main.go
ARG TARGETARCH TARGETOS
RUN CGO_ENABLED=1 GO111MODULE=on GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -tags static,system_libgit2 -a -o gitoperations main.go
FROM alpine:3.15 as runner
WORKDIR /
COPY --from=builder /workspace/gitoperations .
ENTRYPOINT ["/gitoperations"]
Build steps:
docker buildx create --name gitops --use
docker buildx build --platform=linux/amd64,linux/arm64 --pull .
This setup works but the build is taking way too long when building for different arch. The time difference between this specific build step:
RUN CGO_ENABLED=1 GO111MODULE=on GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -tags static,system_libgit2 -a -o gitoperations main.go is always 10x longer when building for different arch:
example:
On arm64 M1 mac (without rossetta): Building arm64 executable takes ~30s and amd64 takes ~300seconds.
On our amd64 Jenkins CI system: Building arm64 executable takes 10x longer than building amd64 executable.
This build times can be seen by looking at the docker buildx build command output.
I believe (and I can most certainly be wrong) its happening because docker is using qemu emulation when building for a cpu architecture thats not the same as host machine's cpu arch. So I want to make use of golang cross-compilation capabilities to speed up the build times.
What I have tried: I thought of having a single builder stage in this dockerfile for arm and amd arch by trying this syntax:
FROM --platform=$BUILDPLATFORM golang:1.17.6-alpine3.15 as builder.
But using the same docker build commands after making this change to dockerfile gives build errors, this is what I get when running on arm64 M1 mac:
> [linux/arm64->amd64 builder 9/9] RUN CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -tags static,system_libgit2 -a -o gitoperations main.go:
#0 1.219 # runtime/cgo
#0 1.219 gcc: error: unrecognized command-line option '-m64'
After reading through golang CGO documentation I think this error is happening because go is not selecting the correct c compiler that is able to build for both architectures and I need to set the CC env variable to instruct go which c compiler to use.
Question: Am I right in assuming that qemu is causing the build time difference and it can be reduced by using golang's native cross-compilation functionality?
How can I make go build compile for amd64 and arm64 from any host machine using docker desktop as I dont have any experience working with C code and gcc and I am not sure what value I should set for CC flag in the go build command if I need to support linux/amd64 and linux/arm64?

Btw I want to share a little bit of my experience, I also tried to build a Go application Docker image using GitHub Actions and go build was run inside the Docker, although the application was not too big, but I felt it was quite a long process. Then I tried to build the binary outside the Docker and the process was much faster, especially if we store the cache from the previous build process.

To be able to compile C code on go you need to set CC variable to arm cross compiler. You can see your CC variable by go env. The error you have is related with the native compiler in the host system you use. You should sudo apt-get install gcc-arm-linux-gnueabi in your dockerfile. After you downloaded necessary cross compilation tools. You need to link your gcc command to compiler you have downloaded via command I mentioned. Then you should be able to compile your application for arm64.
Could you also share your go env output. You might have to edit GOGCCFLAGS variable too.

Yes, you are correct in assuming that qemu is causing the build time difference and using golang's native cross-compilation functionality could help reduce the build time.
To compile for both amd64 and arm64 architectures from any host machine using Docker Desktop, you can set the CC environment variable in the go build command to specify the compiler that Go should use.
For example, to cross-compile for arm64, you can set CC=aarch64-linux-gnu-gcc. For amd64, you can set CC=gcc. You can set these values as arguments in your docker buildx build command.
Note that you would need to have the appropriate cross-compilation tools installed on the host machine for this to work, such as aarch64-linux-gnu-gcc for arm64.

Related

run x64 executable in emulated arm docker

Is it possible to run a x64 executable in a linux arm docker that is emulated on a x64 machine?
I would like to use this to achieve fast cross compilations without changing the architecture of the build system or docker image. I currently compile C++ and C source code in the arm docker but all executables are emulated via QEMU which results in very slow compile times. If the compiler and linker executable were instead x64 executables the whole process would be accelerated.
I know that there is a working alternative for this approach which id like to avoid:
Extract the whole docker filesystem on the host system
Use clang or gcc with the --sysroot argument to cross compile using this extracted filesystem
From the docker side, this is fairly straightforward using buildkit. You would create a Dockerfile that contains something like:
FROM --platform=$BUILDPLATFORM your_build_image as build
WORKDIR /src/
COPY . /src/
ARG TARGETOS
ARG TARGETARCH
RUN make cross-compile-${TARGETOS}-${TARGETARCH}
FROM your_target_image
COPY --from=build /src/app /usr/local/bin/app
The key parts in there:
--platform=$BUILDPLATFORM: this uses an image that matches your builder OS/architecture, rather than the target you are building
${TARGETOS} and ${TARGETARCH} these are injected automatically by buildx and refer to the Go GOOS and GOARCH values (since Docker is written in Go).
The RUN make ... depends on how you build your app for different platforms, change that as appropriate along with adjusting the paths in the COPY.
With that, you can run:
# if you have buildkit enabled and want a single platform image
docker build --platform=linux/amd64 .
# or use buildx for creating multiplatform images, this requires pushing to a registry
docker buildx build --platform=linux/amd64,linux/arm64 --push -t $REGISTRY/$IMAGE:$TAG .
from a linux/arm64 system and BUILDPLATFORM will be set to linux/arm64, TARGETOS=linux, and TARGETARCH=amd64.
You can see more about the automatic build args in the Dockerfile docs.
Beyond that, if you need help doing the actual C/C++ cross compile, that will likely need someone else to help, maybe a separate question including an example of errors you're encountering.

Compile Static Go Binary with Debug Symbols in Separate File?

I can't remember where I saw it, I thought it was on Datadog or on NewRelic or maybe CloudFlare? but I remember someone mentioning that with Golang, they run release binaries in production (of course), and within their Docker containers, they also include a separate file containing debug symbols in case a crash occurs so as to be able to see what happened.
Background
I'm building and running in Docker with a Dockerfile like this:
# do all of our docker building in one image
FROM golang:latest as build
WORKDIR /usr/src/api
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# build the application with relevant flags to make it completely self-contained for a scratch container
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o app
# and then copy the built binary to an empty image
FROM ubuntu:latest
COPY --from=build /usr/src/api/app /
COPY --from=build /usr/src/api/config.defaults.json /config.json
COPY --from=build /usr/src/api/logo.png /
# default to running in a dev environment
ENV ENV=dev
EXPOSE 8080
ENTRYPOINT ["/bin/bash"]
If I don't use the flags above, the binary will fail to execute in alpine and scratch base images:
standard_init_linux.go:219: exec user process caused: no such file or directory
Running this in ubuntu:latest just works, so the above compile flags seem to fix the issue for alpine and scratch.
Question
With this environment in mind, is it possible to have go build emit debug symbols into a separate file to live alongside my static binary in the Docker image?
Use go tool compile using -E flag to Debug symbol export. Is that what you need?
$ go tool compile -E *.go
Type:
go tool compile
for more help regarding how to use it and what are the options available.
Reference:
https://golang.org/cmd/compile/
You don't need to use " -a -installsuffix cgo" flags when building with CGO_ENABLED=0 -- just setting the environment variable will do the trick.
You are building with "-ldflags -s", which is going to strip out all debug symbols and ELF symbol table information. Instead of doing that, do a regular build, archive that executable (in case you need the symbols later) and then remove symbols using strip. E.g.
$ CGO_ENABLED=0 GOOS=linux go build -o app.withsymbols
$ cp app.withsymbols /my/archive/for/debugging/production/issues
$ strip app.withsymbols -o app.stripped
$ cp app.stripped /production/bin
That should give you the behavior you're asking for (e.g. a small production binary, but also a backup binary with symbols for debugging problems in production).

Docker on M1: standard_init_linux.go:219: exec user process caused: exec format error

I tried to create a docker image based on alpine, but whenever I try to run it, I get this error message: standard_init_linux.go:219: exec user process caused: exec format error.
Here's the basic Dockerfile that just runs an executable file:
FROM alpine:3.13.5
WORKDIR /usr/local/bin
COPY profiles-svc /usr/local/bin
EXPOSE 20002/tcp
ENTRYPOINT ["/usr/local/bin/profiles-svc"]
The profiles-svc is an executable generated by the go build command.
I did not notice this issue on my ubuntu laptop, happens only on the Macbook M1.
Thanks in advance for the help!
The issue was that there are two different architectures. If you use go build command on M1, it will be arm64, so if you try to execute that on docker with, for example, alpine image, it will fail. To fix that issue, you need to build amd64 based binary. Here's the command: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out-amd64. Now you can copy that binary to your Linux based dockerfile, build and run.
This is happening because the golang code compiled under Linux arm platform cannot be run under Linux amd platfrom; Similarly, the image built under arm platform will not run on amd platform. The solution is to add the — platform Linux/AMD64 parameter when building your images.
docker build --platform linux/amd64 -t tag .

Undefined References - Golang CGO build fails using Docker, but not on host machine

I'm trying to use the lilliput library for Go. It is only made to run on Linux and OS X.
On my Linux (Debian 10.3) host machine as well as my WSL2 setup (Ubuntu 20.04.1), I have no problems running and building my code (excerpt below).
// main.go
package main
import (
"github.com/discordapp/lilliput"
)
func main() {
...
decoder, err := lilliput.NewDecoder(data)
...
}
However, when I try to put it in a Docker container, with the following configuration, it fails to build.
# Dockerfile v1
FROM golang:1.14.4-alpine AS build
RUN apk add build-base
WORKDIR /src
ENV CGO_ENABLED=1
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN go build -o /out/api .
ENTRYPOINT ["/out/api"]
EXPOSE 8080
I already tried adjusting the Dockerfile with different approaches, for example:
FROM alpine:edge AS build
RUN apk update
RUN apk upgrade
RUN apk add --update go=1.15.3-r0 gcc=10.2.0-r5 g++=10.2.0-r5
WORKDIR /app
RUN go env
ENV GOPATH /app
ADD . /app/src
WORKDIR /app/src
RUN go get -d -v
RUN CGO_ENABLED=1 GOOS=linux go build -o /app/bin/server
FROM alpine:edge
WORKDIR /app
RUN cd /app
COPY --from=build /app/bin/server /app/bin/server
CMD ["bin/server"]
Both result in the following build log:
https://pastebin.com/zMEbEac3
For completeness, the go env of the host machine.
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/kingofdog/.cache/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/kingofdog/go"
GOPROXY=""
GORACE=""
GOROOT="/usr/lib/go-1.11"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go-1.11/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/kingofdog/{PROJECT FOLDER}/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build589460337=/tmp/go-build -gno-record-gcc-switches"
I already searched online for this error, but all I could find dealt with errors in the way others imported C libraries in their Go project. Yet, in my case I'm quite sure that it is not a mistake regarding the source code but rather a configuration mistake of the docker container, as the code itself works perfectly fine outside Docker and I couldn't find a similar issue on the lilliput repository.
The alpine docker image is a minimalistic Linux version - using musl-libc instead of glibc - and is typically used for building tiny images.
To get the more featureful glibc - and resolve your missing CGO dependencies - use the non-alpine version of the golang Docker image to build your asset:
#FROM golang:1.14.4-alpine AS build
#RUN apk add build-base
FROM golang:1.14.4 AS build
Did you build the dependencies?
You have to run the script to build the dependencies on Linux.
Script: https://github.com/discord/lilliput/blob/master/deps/build-deps-linux.sh
Their documentation mentions:
Building Dependencies
Go does not provide any mechanism for arbitrary building of dependencies, e.g. invoking make or cmake. In order to make lilliput usable as a standard Go package, prebuilt static libraries have been provided for all of lilliput's dependencies on Linux and OSX. In order to automate this process, lilliput ships with build scripts alongside compressed archives of the sources of its dependencies. These build scripts are provided for OSX and Linux.
In case it still fails, then issue might be linked to glibc-musl because alpine images have musl libc instead of glibc (GNU's libc). So, you can try it with maybe Ubuntu/ CentOS/etc. minimal images or find a way to get glibc on alpine.

How to use go build to create a dynamic executable binary file that can run in docker container?

I use cross compile to compile a linux version binary on Mac.
GOOS=linux GOARCH=amd64 go build
Also tried the other command that I found on stack overflow.
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o targetBinary
This binary can run in Linux VM. But when I copy this binary to a linux docker container and run it. It can not run anymore.
My docker file is like this
FROM oraclelinux:7.5
COPY targetBinary /
I also use ldd to check target and docker reports this result
[root#7578c118fe27 /]# ldd targetBinary
not a dynamic executable

Resources