How to Build for linux/amd64 and linux/arm64 from macOS

Use Go's cross-compilation flags `GOOS` and `GOARCH` to build binaries for Linux targets directly from macOS without needing a Linux machine.

The Exec format error

You write a Go service on your MacBook. You run go build, scp the binary to your production server, and try to execute it. The server rejects you with Exec format error. You just built an executable for macOS, not Linux. You don't need a Linux VM to fix this. Go has built-in cross-compilation that makes this trivial.

Pointing the compiler

Go's compiler knows how to emit code for many operating systems and CPU architectures. You tell it which one using two environment variables: GOOS for the operating system and GOARCH for the architecture. When you set GOOS=linux, the compiler stops linking against macOS system libraries and starts generating Linux system calls. When you set GOARCH=arm64, it emits ARM instructions instead of x86. The compiler handles the heavy lifting. You just set the knobs.

Minimal cross-compilation

Here's the simplest way to build for Linux from macOS. You set the variables inline before the go build command. The -o flag names the output file so you don't overwrite your local development binary.

# GOOS tells the compiler to target Linux system calls.
# GOARCH tells the compiler to emit x86-64 instructions.
# The -o flag names the output file to avoid overwriting the local binary.
GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 .

Go compiles to the target. You just point the way.

What the compiler does

When you run that command, the Go toolchain reads the environment variables. It switches its internal target configuration. The compiler generates object code for the Linux ABI. The linker produces an ELF binary instead of a Mach-O binary. The result is a file that the Linux kernel can execute. No emulation, no virtual machine, no Docker container required for the build step itself.

Go binaries are statically linked by default. This is a huge advantage for cross-compilation. If you write pure Go, the binary includes all dependencies. You can copy it to any Linux machine with the same architecture and it runs. No missing shared libraries. No ldd errors. This is why Go is popular for microservices and CLI tools. The binary is self-contained.

Building for production

In a real project, you build for multiple architectures. You might want to version the output or organize it. A Makefile is the standard tool for this in Go projects. It keeps the build logic explicit and reproducible.

# Build targets for Linux cross-compilation.
# .PHONY marks these as targets that don't produce files with the same name.
.PHONY: build-linux-amd64 build-linux-arm64 build-linux

# Create the output directory if it doesn't exist.
# This keeps the project root clean from build artifacts.
dist:
	mkdir -p dist

# Build for x86-64 Linux.
# The output goes into dist/ to separate it from local development binaries.
build-linux-amd64: dist
	GOOS=linux GOARCH=amd64 go build -o dist/myapp-amd64 .

# Build for ARM64 Linux.
# Many modern cloud instances and Raspberry Pis use this architecture.
build-linux-arm64: dist
	GOOS=linux GOARCH=arm64 go build -o dist/myapp-arm64 .

# Build both architectures in one command.
# Make runs the dependencies sequentially.
build-linux: build-linux-amd64 build-linux-arm64
	@echo "Built linux/amd64 and linux/arm64 in dist/"

Automate the build. Manual flags are for learning, not for shipping.

The CGO trap

The biggest pitfall is CGO. If your code imports a package that uses C, or you write C code yourself, the Go compiler delegates part of the work to a C compiler. The default C compiler on macOS is Clang, which builds for macOS. It cannot produce Linux code. If you try to cross-compile with CGO enabled, the build fails.

The compiler rejects this with exec: "gcc": executable file not found in $PATH or a linker error about incompatible architectures. You might see runtime/cgo: C compilation failed if the toolchain tries and errors out.

The solution is usually to disable CGO. Go has pure-Go fallbacks for most packages. For example, the net package uses a pure-Go DNS resolver when CGO is disabled. The crypto package uses pure-Go implementations. Disabling CGO forces the compiler to use the portable code paths.

# Disable CGO to force a pure Go build.
# This avoids the need for a C cross-compiler.
# Most Go projects work perfectly fine with CGO disabled.
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 .

CGO breaks cross-compilation. Disable it unless you need C.

Verifying the binary

After building, verify the binary. The file command on macOS reads the header and tells you what you have. It confirms the format and architecture.

# Check the binary format and architecture.
# This confirms the compiler produced the right output.
file myapp-linux-amd64

You should see output like myapp-linux-amd64: ELF 64-bit LSB executable, x86-64. For the ARM binary, you'll see ARM aarch64. If you see Mach-O, you forgot to set GOOS. If you see x86-64 when you wanted arm64, you swapped the flags.

You can also inspect the build metadata embedded in the binary using go version -m. This command shows the target OS, architecture, and build flags. It's more reliable than file because it shows exactly what the Go toolchain recorded.

# Inspect the build metadata embedded in the binary.
# This shows the target OS, architecture, and build flags.
go version -m myapp-linux-amd64

Trust the metadata. Verify before you deploy.

When to cross-compile

Use GOOS and GOARCH environment variables when you need to build for a different operating system or architecture from your current machine. Use CGO_ENABLED=0 when you want a static binary that runs anywhere without external dependencies, or when you don't have a C cross-compiler installed. Use a Makefile or build script when you build for multiple targets regularly and want to avoid typing the same flags every time. Use a dedicated tool like ko or docker buildx when you are deploying to Kubernetes and need to bake the binary into a container image during the build process. Use a Linux VM or container when you must test the binary on the target OS before deployment, because cross-compilation produces the binary but cannot verify runtime behavior.

Cross-compile early. Test on the target late.

Where to go next