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.


