Documentation · Deploying your app

Deploy a Go application

The smallest memory footprint of the common runtimes, one detail that breaks Go deployments everywhere, and building inside the container.

Updated 2026-09-25 · 6 min read

Go services are the comfortable choice on small containers. A statically linked binary typically runs in 10–30 MB, which leaves the rest of a Starter plan for whatever the code actually does. One detail causes nearly every Go deployment failure.

Binding the right interface

Go's ListenAndServe defaults to every interface when you pass an empty host, and the common mistake is writing the address the other way around:

// Correct
addr := ":" + port
http.ListenAndServe(addr, mux)
// Wrong — binds loopback only
http.ListenAndServe("localhost:"+port, mux)

In Go, ":8080" means all interfaces and "0.0.0.0:8080" means the same thing explicitly. "localhost:8080" means loopback only. Writing "http://0.0.0.0:3000" as though it were Node also fails, because that is not a valid listen address.

Reading the port from the environment:

package main

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

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	log.Printf("listening on %s", port)
	log.Fatal(http.ListenAndServe(":"+port, mux))
}

Building inside the container

Ship a source tree and compile on the container. You avoid cross-compilation problems, and the Go module cache makes repeat builds fast.

CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /app/server ./cmd/server

CGO_ENABLED=0 produces a static binary with no libc dependency, which means it runs on Alpine. -trimpath strips local filesystem paths from the binary, and -ldflags="-s -w" drops the symbol table and debug info — smaller binary, and no build paths leaked into your executable.

Set that as the startup command, or build once via the console and set the command to run the binary directly for faster restarts.

Prefer a multi-stage Dockerfile

Building in the container works, but a multi-stage build ships only the binary:

FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server

FROM alpine:latest
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 app
COPY --from=build /out/server /server
USER app
EXPOSE 3000
ENTRYPOINT ["/server"]

The final image contains a binary and nothing else — no toolchain, no source, no module cache. Running as a non-root user costs nothing and removes a class of problems if the service ever handles untrusted input.

Set a read or write timeout

The zero value in Go's http.Server means no timeout at all, which lets a single slow client hold a connection open indefinitely. On a small container, enough of those exhausts the process. Set them explicitly:

srv := &http.Server{
	Addr:         ":" + port,
	Handler:      mux,
	ReadTimeout:  15 * time.Second,
	WriteTimeout: 30 * time.Second,
	IdleTimeout:  60 * time.Second,
}

These guides describe behaviour that is common across container platforms. Where a setting is specific to your service — your assigned port, your SFTP credentials, your startup command — it is shown in the panel rather than here, so check the Startup and Files tabs for your own values.

Found an error or something unclear? Let us know so we can correct it.