Skip to content

Build a Go Application

Target audience: Developers Goal: Build a reproducible Go binary using FROM stagex/pallet-go.

Prerequisites

Create the Project

We'll build serve — a static file HTTP server using only Go's standard library net/http package.

mkdir serve && cd serve

go.mod

module serve

go 1.26

main.go

package main

import (
    "flag"
    "log"
    "net/http"
)

func main() {
    dir := flag.String("d", ".", "directory to serve")
    port := flag.String("p", "8080", "port to listen on")
    flag.Parse()
    log.Printf("Serving %s on :%s", *dir, *port)
    log.Fatal(http.ListenAndServe(":"+*port, http.FileServer(http.Dir(*dir))))
}

Write the Containerfile

Create Containerfile:

FROM docker.io/stagex/pallet-go@sha256:4b7f9fe27d84dd9109fe89c4dab1cefdd66bf2f670817ef95ffd335b84fdf2cb AS build
WORKDIR /app
COPY go.mod main.go ./
RUN CGO_ENABLED=0 GO11MODULE=on go build -trimpath -o /serve

FROM scratch
COPY --from=build /serve /serve
ENTRYPOINT ["/serve"]

Key points:

  • GO11MODULE=on — Enables Go modules. The pallet defaults to GO11MODULE=off for legacy compatibility, so you must opt in explicitly.
  • CGO_ENABLED=0 — Ensures a fully static binary. Go sets this by default when CGO_ENABLED is unset, but being explicit is best practice. If you need CGO, use docker.io/stagex/pallet-cgo@sha256:5feaea6... instead.
  • -trimpath — Removes local filesystem paths from the compiled binary, improving reproducibility.
  • FROM scratch — Go compiles to static binaries. The final image contains only your application — no runtime libraries needed.

Runtime: FROM scratch — Like Rust, Go compiles to fully static binaries. The final image contains only your application. See the Rust guide for more on the FROM scratch pattern, or the Python guide for a contrast with shared-library runtimes.

Build

podman build --timestamp 1 -t serve .

Output:

[1/2] STEP 1/4: FROM docker.io/stagex/pallet-go@sha256:4b7f9fe...
[1/2] STEP 2/4: WORKDIR /app
[1/2] STEP 3/4: COPY go.mod main.go ./
[1/2] STEP 4/4: RUN CGO_ENABLED=0 GO11MODULE=on go build -trimpath -o /serve
[2/2] COMMIT serve

Run

podman run --rm -d -p 8080:8080 --name serve-test serve -d / -p 8080
curl http://127.0.0.1:8080/
podman kill serve-test

The -d / flag tells serve to serve the root filesystem; -p 8080 sets the port. You should see an HTML directory listing.

Adding External Dependencies

External Go module dependencies follow a two-step pattern: fetch (network allowed) then build (network disabled).

Add a dependency to go.mod, then update the Containerfile:

FROM docker.io/stagex/pallet-go@sha256:4b7f9fe27d84dd9109fe89c4dab1cefdd66bf2f670817ef95ffd335b84fdf2cb AS build
WORKDIR /app
COPY go.mod go.sum* ./
COPY main.go ./
RUN GO11MODULE=on go mod download
RUN --network=none <<EOF
    CGO_ENABLED=0 GO11MODULE=on \
    go build -trimpath -o /serve
EOF

FROM scratch
COPY --from=build /serve /serve
ENTRYPOINT ["/serve"]
  • go mod download — Fetches module dependencies from the network. This is the only step that touches the network.
  • go.sum* — Globs go.sum if it exists (avoids error if missing for new projects).
  • RUN --network=none — The actual compilation is hermetic. All modules were already fetched in the previous step.
  • GOPROXY=off is implicit in --network=none — the build step cannot reach any proxy.

For projects without go.mod (legacy), use go get ./... instead of go mod download.

Verify Reproducibility

The --timestamp 1 flag normalizes filesystem and image metadata timestamps to epoch, ensuring the image digest is deterministic. Rebuild with --no-cache and compare digests:

podman build --no-cache --timestamp 1 -t serve:rebuild .
podman inspect serve --format '{{.Digest}}'
podman inspect serve:rebuild --format '{{.Digest}}'

Both digests will be identical — the pinned toolchain, -trimpath, and CGO_ENABLED=0 guarantee deterministic outputs. See the Python guide for an explanation of why --timestamp 1 is necessary for image reproducibility.

See Also