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-secretDatabase 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 deployProduction Checklist
- Multi-stage build with
FROM scratchor minimal base image - Binary compiled with
CGO_ENABLED=0for portability /healthendpoint returns HTTP 200- Port read from
PORTenvironment variable SIGTERMtriggers 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.