Go Application Deployment on Linux

Go applications compile to self-contained static binaries, making deployment on Linux straightforward—no runtime dependencies, no virtual environments, just copy and run. This guide covers compiling Go binaries, creating systemd services, setting up Nginx as a reverse proxy, implementing graceful shutdown, building Docker images, and monitoring Go applications in production.

Prerequisites

  • Ubuntu 20.04+ or CentOS/Rocky 8+ with root access
  • Your Go application source code or a compiled binary
  • Domain name for SSL (optional but recommended)

Installing Go and Compiling Binaries

# Download latest Go (check golang.org for current version)
GO_VERSION="1.22.2"
wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz

# Install
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz

# Add to PATH
echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh
export PATH=$PATH:/usr/local/go/bin

# Verify
go version

Build your application:

cd /path/to/your/app

# Development build
go build -o myapp .

# Production build (optimized, stripped debug info)
CGO_ENABLED=0 go build \
    -ldflags="-s -w -X main.version=1.0.0 -X main.buildTime=$(date -u +%Y%m%dT%H%M%S)" \
    -trimpath \
    -o myapp .

# Verify binary
ls -lh myapp
file myapp   # Should show: ELF 64-bit LSB executable, statically linked

Cross-Compilation

Go makes it easy to build for different architectures on your development machine:

# Build for Linux amd64 from any OS
GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 .

# Build for Linux arm64 (Raspberry Pi, AWS Graviton)
GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 .

# Build for multiple platforms
for OS in linux darwin windows; do
    for ARCH in amd64 arm64; do
        OUTPUT="myapp-${OS}-${ARCH}"
        [ "$OS" = "windows" ] && OUTPUT="${OUTPUT}.exe"
        GOOS=$OS GOARCH=$ARCH go build -o "dist/$OUTPUT" .
        echo "Built $OUTPUT"
    done
done

Setting Up the Application User and Directory

# Create dedicated user (no shell, no home directory needed)
sudo useradd -r -s /bin/false -d /opt/myapp myapp

# Create directories
sudo mkdir -p /opt/myapp/bin
sudo mkdir -p /opt/myapp/config
sudo mkdir -p /var/log/myapp
sudo mkdir -p /etc/myapp

# Copy binary
sudo cp myapp /opt/myapp/bin/myapp
sudo chmod +x /opt/myapp/bin/myapp
sudo chown -R myapp:myapp /opt/myapp
sudo chown myapp:myapp /var/log/myapp

# Create config/env file
sudo tee /etc/myapp/config.env << 'EOF'
APP_ENV=production
HTTP_PORT=8080
DATABASE_URL=postgres://user:pass@localhost/mydb
LOG_LEVEL=info
EOF

sudo chmod 600 /etc/myapp/config.env
sudo chown myapp:myapp /etc/myapp/config.env

systemd Service for Go Applications

sudo tee /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Go Application
Documentation=https://docs.example.com
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp

# Environment
EnvironmentFile=/etc/myapp/config.env

# Working directory
WorkingDirectory=/opt/myapp

# Start command
ExecStart=/opt/myapp/bin/myapp

# Graceful reload via SIGHUP (if your app handles it)
ExecReload=/bin/kill -s HUP $MAINPID

# Restart configuration
Restart=on-failure
RestartSec=5s
StartLimitBurst=5
StartLimitIntervalSec=60s

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/myapp /tmp

# Resource limits
LimitNOFILE=65536
LimitNPROC=4096

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
sudo systemctl status myapp.service

Deploy updates with zero downtime:

#!/bin/bash
# /usr/local/bin/deploy-myapp.sh
set -euo pipefail

NEW_BINARY="$1"
APP_BINARY="/opt/myapp/bin/myapp"
BACKUP_BINARY="/opt/myapp/bin/myapp.prev"

echo "Deploying $NEW_BINARY..."

# Backup current binary
cp "$APP_BINARY" "$BACKUP_BINARY"

# Install new binary
cp "$NEW_BINARY" "$APP_BINARY.new"
chmod +x "$APP_BINARY.new"
mv "$APP_BINARY.new" "$APP_BINARY"

# Graceful reload (Go app must handle SIGHUP for zero-downtime)
if systemctl reload myapp.service 2>/dev/null; then
    echo "Graceful reload successful"
else
    # Fall back to restart if reload not supported
    systemctl restart myapp.service
    echo "Restart completed"
fi

# Verify the service is running
sleep 2
if systemctl is-active --quiet myapp.service; then
    echo "Deployment successful"
else
    echo "Deployment failed, rolling back..."
    cp "$BACKUP_BINARY" "$APP_BINARY"
    systemctl restart myapp.service
    exit 1
fi

Nginx Reverse Proxy

sudo tee /etc/nginx/sites-available/myapp << 'EOF'
upstream go_backend {
    server 127.0.0.1:8080;
    keepalive 32;
}

server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name app.example.com;

    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Proxy to Go app
    location / {
        proxy_pass http://go_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;

        # Timeouts
        proxy_connect_timeout 10s;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;

        # Buffering
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }

    # Health check (bypass auth if you have it)
    location /health {
        proxy_pass http://go_backend;
        access_log off;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Graceful Shutdown Implementation

Add graceful shutdown handling to your Go application:

package main

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

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handleRoot)
    mux.HandleFunc("/health", handleHealth)

    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    // Start server in goroutine
    go func() {
        log.Printf("Server starting on %s", server.Addr)
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    // Wait for interrupt or termination signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
    <-quit

    log.Println("Shutting down gracefully...")

    // Give existing requests 30 seconds to complete
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Printf("Forced shutdown: %v", err)
    }

    log.Println("Server stopped")
}

Docker Deployment with Multi-Stage Build

# Dockerfile - Multi-stage build for minimal image size
FROM golang:1.22-alpine AS builder

WORKDIR /build

# Download dependencies first (caching)
COPY go.mod go.sum ./
RUN go mod download

# Build binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w" \
    -trimpath \
    -o /build/myapp .

# --- Final stage: minimal runtime image ---
FROM scratch

# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy the binary
COPY --from=builder /build/myapp /myapp

EXPOSE 8080

ENTRYPOINT ["/myapp"]
# Build and run
docker build -t myapp:latest .

docker run -d \
  --name myapp \
  --restart unless-stopped \
  -p 127.0.0.1:8080:8080 \
  --env-file /etc/myapp/config.env \
  --memory="256m" \
  --cpus="1.0" \
  myapp:latest

# Check binary size (scratch image is very small)
docker image ls myapp
# Should be ~10-20MB total

Monitoring and Health Checks

Add metrics endpoint to your Go app with expvar or Prometheus:

import (
    "expvar"
    "net/http"
    _ "net/http/pprof"  // pprof endpoints at /debug/pprof/
)

// expvar publishes vars at /debug/vars
var requestCount = expvar.NewInt("requests_total")

func handleRoot(w http.ResponseWriter, r *http.Request) {
    requestCount.Add(1)
    // ...
}

Monitor from the command line:

# Check health
curl -s http://localhost:8080/health | python3 -m json.tool

# View Go runtime stats
curl -s http://localhost:8080/debug/vars | python3 -m json.tool

# Profile CPU (requires pprof)
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

# Profile memory
go tool pprof http://localhost:8080/debug/pprof/heap
# Monitor system resource usage
sudo systemctl status myapp.service
journalctl -u myapp.service -f

# Watch goroutine count (via pprof)
watch -n 5 'curl -s http://localhost:8080/debug/pprof/goroutine?debug=1 | head -5'

Troubleshooting

"address already in use" on startup:

# Find what's using the port
ss -tlnp | grep :8080
sudo fuser 8080/tcp

# Kill the process or change port in config
sudo systemctl stop myapp.service
sudo systemctl start myapp.service

Service starts but immediately exits:

# Check logs
journalctl -u myapp.service -n 50

# Run manually to see startup errors
sudo -u myapp /opt/myapp/bin/myapp

# Check config file exists and is readable
sudo -u myapp cat /etc/myapp/config.env

High memory usage (goroutine leak):

# Check goroutine count
curl -s http://localhost:8080/debug/pprof/goroutine?debug=1 | head -5

# Heap profile
go tool pprof -http=:6060 http://localhost:8080/debug/pprof/heap

Binary won't run: "Exec format error":

# Check architecture
file /opt/myapp/bin/myapp
uname -m

# Recompile for correct architecture
GOOS=linux GOARCH=amd64 go build -o myapp .

Conclusion

Go applications are among the easiest to deploy on Linux—compile a single static binary, set up a systemd service with appropriate security hardening, and place Nginx in front for SSL and load balancing. The multi-stage Docker build produces images as small as 15MB while the graceful shutdown pattern ensures in-flight requests complete before the process exits. Monitor with the built-in pprof endpoints during development, and use Prometheus exporters in production to track goroutine counts, memory usage, and request rates.