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:
- Listen on the port in
PORT, on every interface (":"+port, notlocalhost). - Read its database connection from the environment, usually
DATABASE_URL. - 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
| Option | What you manage | Fits |
|---|---|---|
| A VPS with systemd | The OS, TLS, the database, deploys | One app, full control, lowest cost |
| A container platform (Render, Railway, Fly.io) | The Dockerfile, or nothing | Most apps |
| Kubernetes yourself | Everything | Teams with a platform team |
| Staxa | Nothing per app | An 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
mainpackage must be at the root. The build runsgo build .. If your entry point iscmd/server, commit your own Dockerfile (see the Golang Dockerfile guide). - A newer Go than the one selected still builds.
GOTOOLCHAIN=autodownloads the version in yourgo.modwhen it's newer than the one selected, instead of failing the build. - Commit
go.sumso 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.