Back to Blog
Guide8 min read2026-05-01

Deploying Go Microservices: A Production Guide

A production guide to containerizing and deploying Go microservices with Docker multi-stage builds, health checks, graceful shutdown, and a cloud PaaS.

Deploying Go Microservices: A Production Guide

Go is an excellent choice for microservices — it compiles to a single binary, has a small memory footprint, handles concurrency natively, and produces tiny Docker images. Deploying Go microservices to production is straightforward once you understand the containerization pattern and platform configuration.

This guide covers deploying a production Go microservice on [PandaStack](https://pandastack.io).

Go Binary vs Container

Unlike interpreted languages, Go compiles to a standalone binary. You can run this binary directly on a server, but containers are preferred for production because they:

  • Provide a consistent, reproducible environment
  • Enable zero-downtime deployments via rolling updates
  • Isolate your service from the host operating system
  • Allow the platform to manage scaling and restarts

Multi-Stage Dockerfile for Go

Go's Docker story is particularly clean because the final production image only needs to contain the compiled binary — no language runtime required:

# Stage 1: Build
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Cache dependency download separately from build
COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server ./cmd/server

# Stage 2: Run
FROM scratch

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server

EXPOSE 8080

ENTRYPOINT ["/server"]

Using FROM scratch produces the smallest possible image — just the binary and TLS certificates. The -ldflags="-s -w" flags strip debug symbols, further reducing binary size.

If your service needs the OS filesystem (e.g., for timezone data), use FROM alpine:3.19 as the final stage instead.

Application Structure

// cmd/server/main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/youorg/myservice/internal/api"
)

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

    mux := http.NewServeMux()
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"status":"ok"}`))
    })

    api.RegisterRoutes(mux)

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

    go func() {
        log.Printf("Starting server on :%s", port)
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    // Graceful shutdown
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Forced shutdown: %v", err)
    }
    log.Println("Server stopped gracefully")
}

Configuring pandastack.json

{
  "type": "container",
  "healthCheckPath": "/health"
}

PandaStack uses the healthCheckPath to verify your service is live before routing traffic to it. Always implement this endpoint early — it's the first thing the platform checks after a deployment.

Environment Configuration

Go services should read all configuration from environment variables. Never hardcode database URLs, API keys, or other secrets:

// internal/config/config.go
package config

import (
    "log"
    "os"
)

type Config struct {
    Port        string
    DatabaseURL string
    RedisURL    string
    JWTSecret   string
}

func Load() *Config {
    cfg := &Config{
        Port:        getEnv("PORT", "8080"),
        DatabaseURL: requireEnv("DATABASE_URL"),
        RedisURL:    getEnv("REDIS_URL", ""),
        JWTSecret:   requireEnv("JWT_SECRET"),
    }
    return cfg
}

func requireEnv(key string) string {
    val := os.Getenv(key)
    if val == "" {
        log.Fatalf("Required environment variable %s is not set", key)
    }
    return val
}

func getEnv(key, fallback string) string {
    if val := os.Getenv(key); val != "" {
        return val
    }
    return fallback
}

Set these in the PandaStack dashboard at [dashboard.pandastack.io](https://dashboard.pandastack.io):

DATABASE_URL=postgresql://user:pass@host:5432/mydb
REDIS_URL=redis://redis.internal:6379
JWT_SECRET=your-signing-secret

Database Connection

PandaStack offers managed PostgreSQL, MySQL, Redis, and MongoDB instances. Provision from the Databases section, then use the connection string with pgx or database/sql:

import (
    "database/sql"
    _ "github.com/lib/pq"
)

db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
    log.Fatal(err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)

GitHub Integration and Deployment

Connect your repository from the PandaStack dashboard. PandaStack builds your Docker image from your Dockerfile on every push to your deployment branch. For CLI deploys:

npm install -g @pandastack/cli
panda deploy

Production Checklist

  • Multi-stage build with FROM scratch or minimal base image
  • Binary compiled with CGO_ENABLED=0 for portability
  • /health endpoint returns HTTP 200
  • Port read from PORT environment variable
  • SIGTERM triggers graceful shutdown with timeout
  • Database connection pool size configured
  • All secrets set as environment variables

Deploying Multiple Go Services

A microservices architecture typically involves several independent Go binaries — an API gateway, user service, order service, and so on. On PandaStack, each service is a separate container project with its own GitHub repository or subdirectory. They share managed database and Redis instances provisioned from the PandaStack dashboard.

Services communicate with each other over internal hostnames. Each service exposes its own /health endpoint, and PandaStack monitors them independently. If one service fails its health check, only that service is taken out of rotation — not the entire application.

This separation also means you can deploy individual services independently, rolling out changes to the user service without touching the order service, and each deployment is atomic and zero-downtime.

Visit [docs.pandastack.io](https://docs.pandastack.io) for the full deployment guide.

Ready to deploy?

Start free on PandaStack — no credit card required.

Start free on PandaStack

More in Guide

Browse all Guide articles →

See also