Production Deployment Patterns
Graceful shutdown, structured logging middleware, and running Gin behind Nginx.
Graceful shutdown
Stopping a Go process with os.Exit (or a container orchestrator sending SIGKILL straight away) drops any request currently in flight — the client just sees the connection die mid-response. Graceful shutdown instead stops accepting new connections immediately but gives in-flight requests a deadline to finish naturally before the process actually exits, which matters for every rolling deploy, container restart, or autoscaling-driven termination a production service goes through:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Block until the OS sends an interrupt or termination signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("server forced to shutdown:", err)
}
log.Println("server exiting")
}
srv.Shutdown(ctx) stops the listener from accepting new connections right away, then waits for active requests to complete on their own — up to the 10-second deadline set by ctx here. If requests are still running when that deadline passes, Shutdown gives up and returns, rather than waiting forever for a request that might be stuck. Running Gin directly via router.Run(":8080") (as in the introduction page's minimal example) doesn't expose this control at all — it's a convenience wrapper with no graceful-shutdown hook, which is exactly why production code constructs an explicit http.Server instead.
Structured logging middleware
Gin's default logger (bundled into gin.Default()) prints readable but unstructured plain-text lines — fine for local development, but painful to search or filter once logs are flowing into a centralized aggregator (like an ELK stack, Loki, or Datadog). A structured logging middleware emits each request as a set of key-value fields instead:
import "log/slog"
func StructuredLogger(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
logger.Info("request handled",
slog.String("method", c.Request.Method),
slog.String("path", c.Request.URL.Path),
slog.Int("status", c.Writer.Status()),
slog.Duration("duration", time.Since(start)),
slog.String("client_ip", c.ClientIP()),
)
}
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := gin.New()
r.Use(gin.Recovery(), StructuredLogger(logger))
log/slog is Go's standard-library structured logging package (available since Go 1.21); slog.NewJSONHandler emits each log line as a JSON object, which most log aggregators can index and query directly on fields like status or duration instead of grepping raw text.
Running behind Nginx
The same reverse-proxy pattern covered for ASP.NET Core elsewhere in this app applies equally to Gin — Nginx (or another proxy/load balancer) sits in front, terminating TLS and forwarding plain HTTP to Gin listening on a local port:
server {
listen 80;
server_name api.noalabs.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Two Gin-specific details matter once traffic is arriving through a proxy rather than directly:
- Release mode — set
gin.SetMode(gin.ReleaseMode)(or theGIN_MODE=releaseenvironment variable) in production, disabling the debug warning banner, verbose route-registration logging, and a small amount of debug-only overhead. - Trusted proxies —
c.ClientIP()normally trusts theX-Forwarded-Forheader to determine the real client address, which is only safe if that header genuinely comes from your own proxy and not from an arbitrary client claiming to be someone else. Restrict it explicitly:
r := gin.New()
r.SetTrustedProxies([]string{"127.0.0.1"})
Common mistakes
- Letting a container orchestrator send
SIGKILL(or callingos.Exitdirectly) without ever callingsrv.Shutdown— every in-flight request during a deploy or scale-down event gets its connection dropped instead of finishing normally. - Leaving
gin.SetModeat its default (debug) value in production — the verbose logging and warning banner add noise and a small amount of unnecessary overhead on every request. - Trusting
X-Forwarded-Forforc.ClientIP()without configuringSetTrustedProxies— without it, any client can simply set that header itself and spoof an arbitrary "client IP" in your logs and any IP-based logic.
Interview questions
Q: Why does graceful shutdown matter for a production Gin server, and how is it implemented?
Without it, a deploy, container restart, or scale-down event drops every in-flight request's connection immediately. It's implemented by running Gin through an explicit http.Server (not the router.Run() shorthand) and calling srv.Shutdown(ctx) on receiving SIGINT/SIGTERM — this stops accepting new connections right away but lets active requests finish naturally, up to a deadline.
Q: Why use structured logging middleware instead of Gin's default logger in production?
Gin's default logger prints readable plain-text lines, which are hard to search, filter, or aggregate at scale. Structured logging (e.g., via log/slog with a JSON handler) emits each request as key-value fields a log aggregator can index and query directly — filtering by status code or sorting by duration, for instance — instead of parsing free-form text.
Q: Why is SetTrustedProxies necessary when running Gin behind Nginx?
c.ClientIP() determines the "real" client address partly by trusting the X-Forwarded-For header, which only reflects the truth if it's guaranteed to have been set by your own reverse proxy. Without restricting trusted proxies to that proxy's actual address, any client could set the header itself and spoof a different IP in your application's logs and any IP-based logic.