Go
Anything with a go.mod. Built with your module's toolchain and shipped on distroless.
A Go function is any source tree with a go.mod at its root.
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, "hello")
})
http.ListenAndServe(":"+os.Getenv("PORT"), nil)
}Which package is built
Two conventions cover almost every repository:
main.goat the root → the root package is built.- Exactly one directory under
cmd/→ that one is built.
Anything else is genuinely ambiguous, so it asks rather than guesses. If cmd/
holds several commands, the deploy fails naming them and you set an explicit
main package in settings (for example ./cmd/api). Guessing here would
produce a build that succeeds and runs the wrong binary.
Toolchain version
Taken from the go directive in go.mod, reduced to major.minor because that
is what the toolchain image tags track. An unparseable or missing directive
falls back to Go 1.26 rather than failing the deploy.
The build
Multi-stage, always:
golang:<version>-bookworm gcr.io/distroless/static-debian12:nonroot
├── COPY go.mod go.sum └── COPY --from=build /out/app /app
├── go mod download
├── COPY .
└── go build -trimpath -ldflags="-s -w" -o /out/app <main package>CGO_ENABLED=0, so the binary is static and the runtime image needs no libc.
Multi-stage is not a nicety. A binary shipped inside golang:1.26 is a ~1 GB
image, and image pull is on the cold-start path — which is the product.
The runtime image
| Base (no packages declared) | gcr.io/distroless/static-debian12:nonroot |
| Base (packages declared) | debian:stable-slim |
| Binary | /app |
| Environment | PORT set to the configured port (8080 by default) |
Distroless has no shell and no package manager, which is both the smallest attack surface and the fastest pull. Declaring system packages switches the runtime to Debian, because distroless has no apt — so declare them only when you actually need them.
Setting an explicit entrypoint in settings overrides /app.