Back to Blog
Tutorial11 min read2026-06-29

How to Deploy an Echo Go API with PostgreSQL

Echo is a high-performance, minimalist Go web framework. Learn how to deploy an Echo API to production with PostgreSQL, a tiny static binary, graceful shutdown, and connection pooling.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Echo is one of Go's most popular web frameworks — fast, minimalist, and with a clean middleware model. Go's compiled, statically-linked binaries make it a near-ideal fit for containers: tiny images, fast cold starts, and no runtime to install. This guide deploys an Echo API to production with a managed PostgreSQL database.

Why Go containers are so lean

A Go binary is self-contained. With a multi-stage build you compile in a full toolchain image and ship only the binary in a scratch or distroless final image — often under 20MB. That means fast pulls, fast cold starts, and a minimal attack surface.

A minimal Echo API

// main.go
package main

import (
    "net/http"
    "os"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {
    e := echo.New()
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    e.GET("/", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{"message": "Hello from Echo"})
    })
    e.GET("/health", func(c echo.Context) error {
        return c.String(http.StatusOK, "ok")
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    e.Logger.Fatal(e.Start(":" + port))
}

Echo reads PORT from the environment; binding to :PORT listens on all interfaces, which is what containers need.

Connecting to PostgreSQL with pgx

The pgxpool package is the modern, high-performance way to talk to Postgres from Go:

import (
    "context"
    "os"

    "github.com/jackc/pgx/v5/pgxpool"
)

func newPool(ctx context.Context) (*pgxpool.Pool, error) {
    return pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
}

With a managed PostgreSQL instance linked, DATABASE_URL is injected automatically, and pgx accepts the postgres:// URL directly. Wire the pool into a handler:

e.GET("/users/:id", func(c echo.Context) error {
    id := c.Param("id")
    var name string
    err := pool.QueryRow(c.Request().Context(),
        "SELECT name FROM users WHERE id=$1", id).Scan(&name)
    if err != nil {
        return c.JSON(http.StatusNotFound, map[string]string{"error": "not found"})
    }
    return c.JSON(http.StatusOK, map[string]string{"name": name})
})

Graceful shutdown

Go makes graceful shutdown explicit, and it matters for rolling deploys so in-flight requests aren't dropped:

import (
    "context"
    "os/signal"
    "syscall"
    "time"
)

go func() {
    if err := e.Start(":" + port); err != nil && err != http.ErrServerClosed {
        e.Logger.Fatal(err)
    }
}()

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
<-ctx.Done()

shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(shutdownCtx); err != nil {
    e.Logger.Fatal(err)
}
pool.Close()

This listens for SIGTERM (which the platform sends on deploy), drains connections with a 10-second timeout, then closes the DB pool.

Migrations

Run migrations as a release step. golang-migrate is the common choice:

migrate -database "$DATABASE_URL" -path ./migrations up

Bundle the migrate binary into your image or run it as a separate one-off job before traffic shifts. Don't run migrations from main() on every boot — with multiple replicas you'd get concurrent migration runs racing each other.

The multi-stage Dockerfile

# ---- Build ----
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .

# ---- Runtime ----
FROM gcr.io/distroless/static-debian12
COPY --from=build /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]

Key flags:

  • CGO_ENABLED=0 produces a fully static binary, so it runs in distroless/static or scratch.
  • -ldflags="-s -w" strips debug info to shrink the binary.
  • distroless has no shell or package manager — a tiny, hardened runtime.

Build alternative: buildpacks

If you'd rather not maintain a Dockerfile, Go buildpacks auto-detect your go.mod and main package and produce a runnable image. The explicit Dockerfile gives you the smallest possible image and full control; buildpacks trade a bit of size for zero config.

Environment variables

VariablePurpose
PORTlisten port, injected
DATABASE_URLinjected by managed DB link
LOG_LEVELyour own logging config

Connection pool sizing

pgxpool defaults to a max of max(4, numCPUs) connections. On a small container tier with few cores, this is conservative and sensible. If you run many replicas, multiply per-pod connections by replica count and keep the total under your database's connection limit. Tune via the URL: ?pool_max_conns=10.

Deploying

Push the repo, connect it, link a managed PostgreSQL database so DATABASE_URL is injected, add the migration step, and deploy. The platform builds your multi-stage Dockerfile in an ephemeral build pod, pushes the image, and rolls it out. Live logs confirm the server bound and the pool connected.

git push origin main

Conclusion

Echo plus Go is one of the cleanest deploy stories around: a tiny static binary in a distroless image, pgxpool reading the injected DATABASE_URL, explicit graceful shutdown on SIGTERM, and migrations as a release step. Cold starts are fast and images are tiny.

Try Echo with a managed PostgreSQL on PandaStack's free tier — connect your repo at [dashboard.pandastack.io](https://dashboard.pandastack.io) and the database auto-wires via DATABASE_URL.

References

  • [Echo Framework Documentation](https://echo.labstack.com/docs)
  • [pgx / pgxpool Documentation](https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool)
  • [golang-migrate](https://github.com/golang-migrate/migrate)
  • [Distroless Container Images](https://github.com/GoogleContainerTools/distroless)
  • [Go: Building Minimal Docker Images](https://go.dev/doc/tutorial/docker)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also