Guides · Deployment

Golang hosting: deploy a Go web app with PostgreSQL

What a Go service needs from its host, the Dockerfile Staxa builds for it, and the settings that make it run the same on any platform.

By Jonathan Pitter ·

The short answer

A Go web service is one of the easiest things to host: it compiles to a single static binary with no runtime to install. To run it on any modern platform, the app has to do three things:

  1. Listen on the port in PORT, on every interface (":"+port, not localhost).
  2. Read its database connection from the environment, usually DATABASE_URL.
  3. Shut down cleanly on SIGTERM, so a deploy doesn't cut requests off.

Then you need a host that builds the binary, runs it, puts TLS in front of it and gives it a database. The rest of this guide covers each part, with the code tested against Go 1.27.

Listen on PORT

port := os.Getenv("PORT")
if port == "" {
	port = "8080" // local development
}
srv := &http.Server{Addr: ":" + port, Handler: r}

":" + port listens on every interface. A container's traffic arrives on its network interface, so an app listening on 127.0.0.1 starts fine and never receives a request.

Connect to PostgreSQL

Read the connection string from the environment and open one pool for the whole process. With pgx:

pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil {
	log.Fatal(err)
}
defer pool.Close()

On Staxa, a tenant with a database gets DATABASE_URL set automatically, along with DB_HOST, DB_PORT, DB_NAME, DB_USER and DB_PASSWORD if you'd rather build the DSN yourself. The database runs next to the app inside the tenant's private network, so the URL has sslmode=disable and the host is the tenant's database service.

Run migrations from the binary at startup (with golang-migrate or goose, both of which can embed the SQL files) or as a separate command. Starting up and migrating in the same process is the simplest option when you run one replica.

Shut down on SIGTERM

When a platform replaces a container, it sends SIGTERM and waits a grace period before killing it. http.Server.Shutdown stops accepting connections and waits for in-flight requests:

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
defer stop()

go func() {
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal(err)
	}
}()

<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)

Without this, the process dies mid-request whenever you deploy.

Add a health check route

Platforms decide when a new container is ready by requesting a path until it returns 200. Give them a cheap one:

r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
})

Keep it independent of the database. If the database is briefly unavailable, you want the app to keep serving what it can, not to be pulled out of rotation.

What to look for in Go hosting

OptionWhat you manageFits
A VPS with systemdThe OS, TLS, the database, deploysOne app, full control, lowest cost
A container platform (Render, Railway, Fly.io)The Dockerfile, or nothingMost apps
Kubernetes yourselfEverythingTeams with a platform team
StaxaNothing per appAn app run once per customer, each with its own database

Go doesn't need much from a host: no language runtime, low memory (our test service idled at 8 MB), and fast cold starts. What matters is how deploys, TLS, databases and logs are handled. Our Heroku alternatives comparison covers the container platforms in detail.

Deploying a Go app on Staxa

Connect the GitHub repository. Staxa detects Go from go.mod at the root and, if the repository has no Dockerfile, generates this one (shown for port 8080 with Go 1.27 selected):

FROM golang:1.27-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git ca-certificates
# The image pins its own Go (GOTOOLCHAIN=local). auto fetches the newer
# toolchain a go.mod asks for instead of failing the build.
ENV GOTOOLCHAIN=auto
# go.sum is optional: a module with no dependencies has none.
COPY go.mod go.sum* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o app .

FROM alpine:3.22
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/app .
RUN adduser -D -u 1001 app
USER app
ENV PORT=8080
EXPOSE 8080
CMD ["./app"]

What this means for your repository:

  • The main package must be at the root. The build runs go build .. If your entry point is cmd/server, commit your own Dockerfile (see the Golang Dockerfile guide).
  • A newer Go than the one selected still builds. GOTOOLCHAIN=auto downloads the version in your go.mod when it's newer than the one selected, instead of failing the build.
  • Commit go.sum so builds use the dependency versions you tested.
  • cgo is off. A dependency that needs it (such as mattn/go-sqlite3) needs your own Dockerfile.
  • The image is about 25 MB for a small service, and runs as an unprivileged user.

Staxa sets PORT to the service's port, routes https://<service>-<tenant>.staxa.app to it with TLS, and checks readiness with GET / unless you set a health check path (use /healthz from above). The build log shows the generated Dockerfile. If your repository has a Dockerfile, Staxa builds that instead.

The Go and PostgreSQL demo is a complete contact manager that migrates its own schema at startup. There are also demos for Fiber and Echo.

Common problems

The deploy never becomes ready

The readiness check gets no answer or no 200. Check that the app listens on PORT and on every interface, and that GET / (or your health check path) returns 200 without authentication.

The build fails with "go.mod requires go >= X"

The image's Go is older than your go.mod and the Dockerfile doesn't set GOTOOLCHAIN=auto. Staxa's generated Dockerfile does; add it to your own, or use a newer base image.

Connection refused to the database

DATABASE_URL is only set when the tenant has a database. Check the tenant's environment, and that the code reads the variable at runtime rather than from a file baked into the image.

Frequently asked questions

Do I need a Dockerfile to host a Go app?

Not on Staxa, or on platforms with buildpacks. You need one when the build is anything other than go build . at the root, or when you need cgo.

How much memory does a Go web service need?

A small JSON API idles at under 10 MB and rarely needs more than 128 MB under moderate load. Go's garbage collector uses memory it's given, so set GOMEMLIMIT a little under the container's limit if you see it grow.

Which Go web framework should I use?

The standard library's net/http has had method and path-parameter routing since Go 1.22, which covers many APIs. chi, Echo, Fiber and Gin all host the same way: they listen on a port like any other Go server.

Give every customer their own stack

Staxa provisions an isolated app, database, subdomain and TLS per customer from one API call. The Free plan includes 2 tenants.