Skip to main content

Echo Production Configuration

When you're ready to deploy your Echo application to production, there are several important configuration considerations to ensure your application is secure, performant, and reliable. This guide will walk you through the essential production configuration steps for your Echo web applications.

Introduction

Moving an Echo application from development to production requires careful configuration to handle real-world traffic, security threats, and performance demands. Production environments differ significantly from development environments, and proper configuration is crucial for maintaining application stability and security.

In this guide, we'll cover:

  • Server configuration
  • Security settings
  • Logging
  • Performance optimization
  • Error handling
  • Health checks and monitoring

Basic Production Server Configuration

Let's start with a basic production-ready Echo server configuration:

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"time"
)

func main() {
// Create a new Echo instance
e := echo.New()

// Production configurations
e.Debug = false // Disable debug mode in production

// Start server with graceful shutdown
s := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Minute,
WriteTimeout: 5 * time.Minute,
IdleTimeout: 120 * time.Second,
}

e.Logger.Fatal(e.StartServer(s))
}

In this basic setup, we've disabled debug mode and configured timeouts which are crucial for preventing resource exhaustion in production environments.

Essential Production Middleware

Echo provides several middleware components that are especially important in production:

go
// Add production middleware
e.Use(middleware.Recover())
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "SAMEORIGIN",
HSTSMaxAge: 31536000,
HSTSExcludeSubdomains: false,
ContentSecurityPolicy: "default-src 'self'",
}))
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Level: 5, // Default compression level
}))
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20))) // 20 requests per second

Each of these middleware components serves an important purpose:

  1. Recover: Catches panics and converts them to errors
  2. Secure: Sets various security headers to protect against common web vulnerabilities
  3. Gzip: Compresses responses to improve performance
  4. RateLimiter: Prevents abuse by limiting request rates

Configuring Proper Logging

In production, structured logging is essential for monitoring and debugging:

go
// Configure structured JSON logger
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` +
`"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` +
`"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` +
`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
Output: os.Stdout,
}))

You might also want to redirect logs to a file or external logging service:

go
// Log to file
logFile, err := os.OpenFile("echo.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
e.Logger.Fatal(err)
}
e.Logger.SetOutput(logFile)

CORS Configuration for Production

If your API serves clients from different domains, you'll need to configure CORS carefully:

go
// Configure CORS for production
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourdomain.com", "https://www.yourdomain.com"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
MaxAge: 86400, // 24 hours
}))

This configuration restricts access to specific domains, methods, and headers, which is much more secure than the permissive settings often used during development.

TLS Configuration for HTTPS

HTTPS is non-negotiable for production applications. Here's how to configure TLS:

go
// TLS configuration
e.Logger.Fatal(e.StartTLS(":443", "cert.pem", "key.pem"))

For more advanced TLS configurations:

go
// Custom TLS configuration
s := &http.Server{
Addr: ":443",
Handler: e,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.X25519,
},
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
},
}

e.Logger.Fatal(s.ListenAndServeTLS("cert.pem", "key.pem"))

Database Connection Management

Properly managing database connections is crucial in production:

go
// Configure database with connection pooling
db, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname")
if err != nil {
e.Logger.Fatal(err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)

These settings help prevent connection exhaustion and ensure efficient resource usage.

Health Check Endpoint

A health check endpoint is essential for container orchestration systems and load balancers:

go
// Health check endpoint
e.GET("/health", func(c echo.Context) error {
// Check dependencies like database, cache, etc.
if dbErr := db.Ping(); dbErr != nil {
return c.JSON(http.StatusServiceUnavailable, map[string]string{
"status": "error",
"message": "Database connection failed",
})
}

return c.JSON(http.StatusOK, map[string]string{
"status": "ok",
"version": "1.0.0",
})
})

Graceful Shutdown

Properly shutting down your application ensures that in-flight requests are completed:

go
// Graceful shutdown
go func() {
if err := e.StartServer(s); err != nil && err != http.ErrServerClosed {
e.Logger.Fatal("shutting down the server")
}
}()

// Wait for interrupt signal to gracefully shut down the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}

Complete Production Configuration Example

Here's a complete example that incorporates all the production best practices we've discussed:

go
package main

import (
"context"
"database/sql"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"os"
"os/signal"
"time"

_ "github.com/lib/pq" // PostgreSQL driver
)

func main() {
// Create a new Echo instance
e := echo.New()

// Production configurations
e.Debug = false

// Connect to database
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
e.Logger.Fatal(err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
defer db.Close()

// Add production middleware
e.Use(middleware.Recover())
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "SAMEORIGIN",
HSTSMaxAge: 31536000,
HSTSExcludeSubdomains: false,
ContentSecurityPolicy: "default-src 'self'",
}))
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Level: 5,
}))
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))

// Configure structured logging
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` +
`"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` +
`"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` +
`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
Output: os.Stdout,
}))

// Configure CORS
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourdomain.com", "https://www.yourdomain.com"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
MaxAge: 86400,
}))

// Health check endpoint
e.GET("/health", func(c echo.Context) error {
if dbErr := db.Ping(); dbErr != nil {
return c.JSON(http.StatusServiceUnavailable, map[string]string{
"status": "error",
"message": "Database connection failed",
})
}

return c.JSON(http.StatusOK, map[string]string{
"status": "ok",
"version": "1.0.0",
})
})

// Your routes go here
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, Production!")
})

// Start server with graceful shutdown
s := &http.Server{
Addr: ":" + os.Getenv("PORT"),
ReadTimeout: 5 * time.Minute,
WriteTimeout: 5 * time.Minute,
IdleTimeout: 120 * time.Second,
}

go func() {
if err := e.StartServer(s); err != nil && err != http.ErrServerClosed {
e.Logger.Fatal("shutting down the server")
}
}()

// Wait for interrupt signal to gracefully shut down the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
}

Environment-based Configuration

In practice, you'll want to load configuration from environment variables or configuration files:

go
// Load configuration from environment variables
port := os.Getenv("PORT")
if port == "" {
port = "8080" // Default port
}

// Use environment-specific settings
if os.Getenv("ENVIRONMENT") == "production" {
// Production-specific settings
e.Debug = false
// More production settings...
} else {
// Development settings
e.Debug = true
// More development settings...
}

Summary

Properly configuring your Echo application for production is crucial for security, performance, and reliability. Key considerations include:

  1. Security: Disable debug mode, use secure headers, configure CORS, and enable HTTPS
  2. Performance: Set appropriate timeouts, use compression, and configure connection pools
  3. Reliability: Implement graceful shutdown, health checks, and proper error handling
  4. Monitoring: Configure structured logging for better observability

By following these best practices, you'll be well on your way to a robust, production-ready Echo application that can handle real-world traffic and challenges.

Additional Resources and Exercises

Resources

Exercises

  1. Security Audit: Review your Echo application's security settings against the OWASP Top 10 vulnerabilities.

  2. Load Testing: Use a tool like hey or wrk to load test your Echo application and adjust configurations for optimal performance:

    bash
    # Install hey
    go get -u github.com/rakyll/hey

    # Run a load test
    hey -n 10000 -c 100 http://localhost:8080/
  3. Configuration Management: Implement a configuration system that loads settings from environment variables, flags, and configuration files with appropriate precedence.

  4. Monitoring Integration: Extend the health check endpoint to include more comprehensive application health metrics, and integrate with a monitoring solution like Prometheus.

  5. Dockerization: Create a production-ready Dockerfile for your Echo application that follows security best practices, including running as a non-root user.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)