Install and configure Go web applications with systemd and reverse proxy

Beginner 25 min Apr 17, 2026 11 views
Ubuntu 24.04 Ubuntu 22.04 Debian 12 AlmaLinux 9 Rocky Linux 9 Fedora 41

Deploy Go web applications in production with systemd service management, NGINX reverse proxy with SSL termination, and comprehensive logging for monitoring and maintenance.

Prerequisites

  • Root or sudo access
  • Domain name for SSL certificates
  • Basic Linux command line knowledge

What this solves

Go applications need proper service management, reverse proxy configuration, and SSL termination for production deployment. This tutorial sets up a complete production environment with systemd for process management, NGINX for reverse proxy and load balancing, and structured logging for monitoring.

Step-by-step installation

Update system packages

Start by updating your package manager to ensure you get the latest versions of all packages.

sudo apt update && sudo apt upgrade -y
sudo dnf update -y

Install Go compiler and development tools

Install the Go compiler and essential development tools for building web applications.

sudo apt install -y golang-go git curl nginx certbot python3-certbot-nginx
sudo dnf install -y golang git curl nginx certbot python3-certbot-nginx

Verify Go installation

Check that Go is properly installed and configured with the correct version.

go version
go env GOPATH GOROOT

Create application user and directory structure

Create a dedicated user for running the Go application with proper permissions and directory structure.

sudo useradd --system --shell /bin/false --home /opt/goapp goapp
sudo mkdir -p /opt/goapp/{bin,logs,config}
sudo chown -R goapp:goapp /opt/goapp

Create sample Go web application

Create a production-ready Go web application with structured logging and graceful shutdown handling.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

type Server struct {
    httpServer *http.Server
    logger     *log.Logger
}

type Response struct {
    Message   string    json:"message"
    Timestamp time.Time json:"timestamp"
    Version   string    json:"version"
}

func NewServer() *Server {
    logger := log.New(os.Stdout, "[GOAPP] ", log.LstdFlags|log.Lshortfile)
    
    mux := http.NewServeMux()
    
    server := &Server{
        httpServer: &http.Server{
            Addr:         ":8080",
            Handler:      mux,
            ReadTimeout:  15 * time.Second,
            WriteTimeout: 15 * time.Second,
            IdleTimeout:  60 * time.Second,
        },
        logger: logger,
    }
    
    mux.HandleFunc("/", server.handleHome)
    mux.HandleFunc("/health", server.handleHealth)
    mux.HandleFunc("/api/status", server.handleAPIStatus)
    
    return server
}

func (s Server) handleHome(w http.ResponseWriter, r http.Request) {
    s.logger.Printf("Home request from %s", r.RemoteAddr)
    
    response := Response{
        Message:   "Go web application is running",
        Timestamp: time.Now(),
        Version:   "1.0.0",
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func (s Server) handleHealth(w http.ResponseWriter, r http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, {"status":"healthy","timestamp":"%s"}, time.Now().Format(time.RFC3339))
}

func (s Server) handleAPIStatus(w http.ResponseWriter, r http.Request) {
    s.logger.Printf("API status request from %s", r.RemoteAddr)
    
    response := map[string]interface{}{
        "status":    "operational",
        "uptime":    time.Since(startTime).String(),
        "timestamp": time.Now(),
        "version":   "1.0.0",
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func (s *Server) Start() error {
    s.logger.Printf("Starting server on %s", s.httpServer.Addr)
    return s.httpServer.ListenAndServe()
}

func (s *Server) Shutdown(ctx context.Context) error {
    s.logger.Println("Shutting down server gracefully")
    return s.httpServer.Shutdown(ctx)
}

var startTime = time.Now()

func main() {
    server := NewServer()
    
    go func() {
        if err := server.Start(); err != nil && err != http.ErrServerClosed {
            server.logger.Fatalf("Server failed to start: %v", err)
        }
    }()
    
    server.logger.Println("Server started successfully")
    
    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 := server.Shutdown(ctx); err != nil {
        server.logger.Fatalf("Server forced to shutdown: %v", err)
    }
    
    server.logger.Println("Server stopped gracefully")
}

Build the Go application

Compile the Go application with proper optimization flags for production deployment.

cd /opt/goapp
sudo -u goapp go mod init goapp
sudo -u goapp go mod tidy
sudo -u goapp go build -ldflags "-s -w" -o bin/goapp main.go

Create systemd service configuration

Configure systemd to manage the Go application with proper restart policies and resource limits.

[Unit]
Description=Go Web Application
After=network.target
Wants=network.target

[Service]
Type=exec
User=goapp
Group=goapp
WorkingDirectory=/opt/goapp
ExecStart=/opt/goapp/bin/goapp
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
TimeoutStopSec=30

Security settings

NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/opt/goapp/logs

Resource limits

LimitNOFILE=65536 MemoryMax=512M

Logging

StandardOutput=append:/opt/goapp/logs/goapp.log StandardError=append:/opt/goapp/logs/goapp-error.log SyslogIdentifier=goapp [Install] WantedBy=multi-user.target

Create log rotation configuration

Configure logrotate to manage application logs and prevent disk space issues.

/opt/goapp/logs/*.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    notifempty
    create 0644 goapp goapp
    postrotate
        /bin/systemctl reload goapp.service > /dev/null 2>&1 || true
    endscript
}

Enable and start the Go application service

Enable the systemd service to start automatically on boot and start it now.

sudo systemctl daemon-reload
sudo systemctl enable goapp.service
sudo systemctl start goapp.service
sudo systemctl status goapp.service

Configure NGINX reverse proxy

Set up NGINX as a reverse proxy with proper headers, caching, and security configurations.

upstream goapp_backend {
    server 127.0.0.1:8080;
    keepalive 32;
}

server {
    listen 80;
    server_name example.com www.example.com;
    
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
    # Logging
    access_log /var/log/nginx/goapp_access.log combined;
    error_log /var/log/nginx/goapp_error.log warn;
    
    # Health check endpoint
    location /health {
        proxy_pass http://goapp_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_cache_bypass 1;
        proxy_no_cache 1;
        access_log off;
    }
    
    # API endpoints with caching
    location /api/ {
        proxy_pass http://goapp_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
        
        proxy_cache_bypass $http_upgrade;
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        
        # Rate limiting
        limit_req zone=api burst=20 nodelay;
    }
    
    # Main application
    location / {
        proxy_pass http://goapp_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
        
        proxy_cache_bypass $http_upgrade;
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        
        # Basic caching for static responses
        proxy_cache_valid 200 1m;
    }
}

Rate limiting configuration

http { limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; }

Configure NGINX main settings

Update the main NGINX configuration with performance optimizations and security settings.

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024;
    use epoll;
    multi_accept on;
}

http {
    # Basic Settings
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 1000;
    types_hash_max_size 2048;
    server_tokens off;
    
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    
    # MIME types
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # SSL Settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    
    # Logging Settings
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                   '$status $body_bytes_sent "$http_referer" '
                   '"$http_user_agent" "$http_x_forwarded_for" '
                   'rt=$request_time uct="$upstream_connect_time" '
                   'uht="$upstream_header_time" urt="$upstream_response_time"';
    
    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;
    
    # Gzip Settings
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;
    
    # Virtual Host Configs
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Enable NGINX site configuration

Enable the Go application site and remove the default NGINX configuration.

sudo ln -sf /etc/nginx/sites-available/goapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginx

Configure firewall rules

Set up firewall rules to allow HTTP, HTTPS, and SSH traffic while blocking direct access to the Go application port.

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Configure SSL certificates with Let's Encrypt

Set up automatic SSL certificates using Let's Encrypt and Certbot for production security.

Note: Replace example.com with your actual domain name. Ensure your domain points to this server's IP address.
sudo certbot --nginx -d example.com -d www.example.com --non-interactive --agree-tos --email admin@example.com --redirect

Set up automatic SSL renewal

Configure automatic SSL certificate renewal to maintain security without manual intervention.

sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
sudo systemctl status certbot.timer

Verify your setup

Test that all components are working correctly with these verification commands:

# Check Go application service status
sudo systemctl status goapp.service

Check NGINX status

sudo systemctl status nginx

Test application directly

curl -i http://localhost:8080/health

Test through NGINX proxy

curl -i http://localhost/health

Test HTTPS (if SSL configured)

curl -i https://example.com/health

Check application logs

sudo tail -f /opt/goapp/logs/goapp.log

Check NGINX logs

sudo tail -f /var/log/nginx/goapp_access.log

You should see JSON responses from the health endpoints and successful HTTP status codes. For more advanced monitoring, consider setting up Prometheus and Grafana monitoring for comprehensive application metrics.

Production monitoring and logging

Configure structured logging

Set up structured JSON logging for better log parsing and monitoring integration.

package main

import (
    "encoding/json"
    "log"
    "os"
    "time"
)

type LogEntry struct {
    Level     string    json:"level"
    Message   string    json:"message"
    Timestamp time.Time json:"timestamp"
    Service   string    json:"service"
    IP        string    json:"client_ip,omitempty"
    Method    string    json:"method,omitempty"
    Path      string    json:"path,omitempty"
    Status    int       json:"status,omitempty"
    Duration  float64   json:"duration_ms,omitempty"
}

func LogInfo(message string, fields map[string]interface{}) {
    entry := LogEntry{
        Level:     "info",
        Message:   message,
        Timestamp: time.Now(),
        Service:   "goapp",
    }
    
    // Add custom fields
    if ip, ok := fields["ip"].(string); ok {
        entry.IP = ip
    }
    if method, ok := fields["method"].(string); ok {
        entry.Method = method
    }
    if path, ok := fields["path"].(string); ok {
        entry.Path = path
    }
    if status, ok := fields["status"].(int); ok {
        entry.Status = status
    }
    if duration, ok := fields["duration"].(float64); ok {
        entry.Duration = duration
    }
    
    if jsonData, err := json.Marshal(entry); err == nil {
        log.Println(string(jsonData))
    }
}

Configure application monitoring

Add basic metrics collection and monitoring endpoints for operational visibility.

package main

import (
    "encoding/json"
    "net/http"
    "runtime"
    "sync/atomic"
    "time"
)

type Metrics struct {
    RequestCount   int64     json:"request_count"
    ErrorCount     int64     json:"error_count"
    Uptime         string    json:"uptime"
    GoRoutines     int       json:"goroutines"
    MemoryUsage    uint64    json:"memory_usage_bytes"
    LastRequestTime time.Time json:"last_request_time"
}

var (
    requestCount int64
    errorCount   int64
    lastRequest  time.Time
)

func incrementRequestCount() {
    atomic.AddInt64(&requestCount, 1)
    lastRequest = time.Now()
}

func incrementErrorCount() {
    atomic.AddInt64(&errorCount, 1)
}

func (s Server) handleMetrics(w http.ResponseWriter, r http.Request) {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    metrics := Metrics{
        RequestCount:    atomic.LoadInt64(&requestCount),
        ErrorCount:      atomic.LoadInt64(&errorCount),
        Uptime:          time.Since(startTime).String(),
        GoRoutines:      runtime.NumGoroutine(),
        MemoryUsage:     m.Alloc,
        LastRequestTime: lastRequest,
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(metrics)
}

Common issues

SymptomCauseFix
Service fails to start Port already in use or permission denied sudo lsof -i :8080 and sudo systemctl status goapp.service
502 Bad Gateway from NGINX Go application not running or connection refused curl localhost:8080/health and check service status
Permission denied on log files Incorrect file ownership or permissions sudo chown -R goapp:goapp /opt/goapp/logs and sudo chmod 755 /opt/goapp/logs
SSL certificate errors Domain not pointing to server or Certbot failure Check DNS with dig example.com and retry sudo certbot --nginx
High memory usage Memory leaks or insufficient garbage collection Review application code and add GOGC=100 environment variable
NGINX configuration test fails Syntax errors in configuration files sudo nginx -t for detailed error messages
Never use chmod 777. It gives every user on the system full access to your files. Instead, fix ownership with chown and use minimal permissions like 755 for directories and 644 for files.

Next steps

Running this in production?

Want this handled for you? This works for a single server. When you run multiple environments or need this available 24/7, keeping it healthy is a different job. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.