Deployment Strategies for Go Applications

Deployment Strategies for Go Applications

Deploying Go applications is straightforward due to Go’s ability to create static binaries. This guide covers various deployment strategies from simple binary deployment to container orchestration.

Building Go Applications

Basic Build

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Build commands:

# Build for current platform
go build -o myapp .

# Build for specific OS and architecture
GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 .
GOOS=windows GOARCH=amd64 go build -o myapp-windows-amd64.exe .
GOOS=darwin GOARCH=amd64 go build -o myapp-darwin-amd64 .

# Build with optimizations
go build -ldflags="-s -w" -o myapp .  # Strip debug info

# Build with version info
go build -ldflags="-X main.version=1.2.3 -X main.buildTime=$(date -u +%Y%m%d%H%M%S)" -o myapp .

Docker Deployment

Basic Dockerfile

# Multi-stage build
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o myapp .

# Final stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy the binary from builder stage
COPY --from=builder /app/myapp .

# Expose port
EXPOSE 8080

# Run the binary
CMD ["./myapp"]

Advanced Dockerfile with Configuration

FROM golang:1.21-alpine AS builder

# Install git (needed for private modules)
RUN apk add --no-cache git

WORKDIR /app

# Copy and download dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy source
COPY . .

# Build with build info
ARG VERSION=dev
ARG BUILD_TIME
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" \
    -o myapp .

FROM alpine:latest

# Install necessary runtime dependencies
RUN apk --no-cache add ca-certificates tzdata

# Create non-root user
RUN adduser -D -s /bin/sh appuser

WORKDIR /home/appuser/

# Copy binary and config
COPY --from=builder /app/myapp .
COPY --from=builder /app/config.yaml .

# Change ownership
RUN chown appuser:appuser myapp config.yaml

# Switch to non-root user
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

EXPOSE 8080

CMD ["./myapp"]

Building and Running

# Build Docker image
docker build -t myapp:latest .

# Build with build args
docker build \
    --build-arg VERSION=1.2.3 \
    --build-arg BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
    -t myapp:1.2.3 .

# Run container
docker run -p 8080:8080 myapp:latest

# Run with environment variables
docker run -p 8080:8080 -e DATABASE_URL=postgres://... myapp:latest

Systemd Service

Deploying as a systemd service on Linux:

Create systemd service file

[Unit]
Description=My Go Application
After=network.target

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp
Restart=always
RestartSec=5
Environment=DATABASE_URL=postgres://localhost/myapp
Environment=PORT=8080

# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/myapp/logs
ProtectHome=true

[Install]
WantedBy=multi-user.target

Deployment script

#!/bin/bash

# Stop service if running
sudo systemctl stop myapp || true

# Create application directory
sudo mkdir -p /opt/myapp
sudo chown appuser:appuser /opt/myapp

# Copy binary and config
sudo cp myapp /opt/myapp/
sudo cp config.yaml /opt/myapp/
sudo chown appuser:appuser /opt/myapp/*

# Copy systemd service file
sudo cp myapp.service /etc/systemd/system/

# Reload systemd and start service
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp

# Check status
sudo systemctl status myapp

Cloud Deployment

AWS EC2 Deployment

#!/bin/bash

# Update system
sudo yum update -y

# Install Go (if not using container)
# wget https://golang.org/dl/go1.21.0.linux-amd64.tar.gz
# sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
# export PATH=$PATH:/usr/local/go/bin

# Create application user
sudo useradd -m -s /bin/bash appuser

# Create application directory
sudo mkdir -p /opt/myapp
sudo chown appuser:appuser /opt/myapp

# Copy application files (use scp, rsync, or CI/CD)
# scp myapp ec2-user@instance:/opt/myapp/
# scp config.yaml ec2-user@instance:/opt/myapp/

# Install as systemd service
sudo cp myapp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp

# Configure firewall
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload

# Setup log rotation
sudo cp /etc/logrotate.d/myapp /etc/logrotate.d/
sudo logrotate /etc/logrotate.conf

Heroku Deployment

Create Procfile:

web: myapp

Build with correct settings:

# Build for Heroku (dyno uses Linux)
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o myapp .

# Create .profile for runtime configuration
echo "export PORT=\$PORT" > .profile

Google Cloud Run

# cloudbuild.yaml
steps:
  - name: 'golang:1.21'
    args: ['go', 'build', '-ldflags=-s -w', '-o', 'myapp', '.']
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 'gcr.io/$PROJECT_ID/myapp:$COMMIT_SHA', '.']
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'gcr.io/$PROJECT_ID/myapp:$COMMIT_SHA']
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: gcloud
    args:
      - 'run'
      - 'deploy'
      - 'myapp'
      - '--image'
      - 'gcr.io/$PROJECT_ID/myapp:$COMMIT_SHA'
      - '--region'
      - 'us-central1'
      - '--platform'
      - 'managed'
      - '--port'
      - '8080'

Kubernetes Deployment

Dockerfile for Kubernetes

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o myapp .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]

Kubernetes manifests

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: myapp-secrets
              key: database-url
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"

---
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8080
  type: LoadBalancer

---
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  config.yaml: |
    server:
      host: "0.0.0.0"
      port: 8080
    log_level: "info"    

---
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: myapp-secrets
type: Opaque
data:
  database-url: <base64-encoded-database-url>

Deployment commands

# Apply manifests
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml

# Check deployment
kubectl get pods
kubectl get services
kubectl logs -f deployment/myapp

# Scale deployment
kubectl scale deployment myapp --replicas=5

# Update deployment
kubectl set image deployment/myapp myapp=myapp:v2.0.0

CI/CD with GitHub Actions

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: 1.21
    
    - name: Cache Go modules
      uses: actions/cache@v3
      with:
        path: ~/go/pkg/mod
        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
        restore-keys: |
          ${{ runner.os }}-go-          
    
    - name: Build
      run: |
        CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o myapp .        
    
    - name: Build Docker image
      run: |
        docker build -t myapp:${{ github.sha }} .
        docker tag myapp:${{ github.sha }} myapp:latest        
    
    - name: Deploy to staging
      if: github.ref == 'refs/heads/main'
      run: |
        echo "Deploy to staging environment"
        # Add your deployment commands here        
    
    - name: Run tests on staging
      if: github.ref == 'refs/heads/main'
      run: |
        # Health check
        curl -f http://staging.example.com/health
        # Run integration tests
        # ...        
    
    - name: Deploy to production
      if: github.ref == 'refs/heads/main' && github.event_name == 'push'
      run: |
        echo "Deploy to production environment"
        # Add your production deployment commands here        

Blue-Green Deployment

#!/bin/bash

# Blue-green deployment script
BLUE="myapp-blue"
GREEN="myapp-green"

# Determine which environment is live
if kubectl get service myapp-service -o jsonpath='{.spec.selector.version}' | grep -q blue; then
    LIVE=$BLUE
    STANDBY=$GREEN
    LIVE_VERSION="blue"
    STANDBY_VERSION="green"
else
    LIVE=$GREEN
    STANDBY=$BLUE
    LIVE_VERSION="green"
    STANDBY_VERSION="blue"
fi

echo "Live environment: $LIVE"
echo "Standby environment: $STANDBY"

# Deploy to standby environment
kubectl set image deployment/$STANDBY myapp=myapp:$NEW_VERSION
kubectl rollout status deployment/$STANDBY

# Wait for standby to be ready
kubectl wait --for=condition=available --timeout=300s deployment/$STANDBY

# Run smoke tests on standby
if curl -f http://standby.example.com/health; then
    echo "Smoke tests passed"
else
    echo "Smoke tests failed, rolling back"
    kubectl rollout undo deployment/$STANDBY
    exit 1
fi

# Switch traffic to standby
kubectl patch service myapp-service -p "{\"spec\":{\"selector\":{\"app\":\"myapp\",\"version\":\"$STANDBY_VERSION\"}}}"

# Wait for traffic switch
sleep 30

# Verify new version is working
if curl -f http://myapp.example.com/health; then
    echo "Traffic switch successful"
    
    # Scale down old version
    kubectl scale deployment $LIVE --replicas=0
else
    echo "Traffic switch failed, rolling back"
    kubectl patch service myapp-service -p "{\"spec\":{\"selector\":{\"app\":\"myapp\",\"version\":\"$LIVE_VERSION\"}}}"
    kubectl scale deployment $STANDBY --replicas=0
    exit 1
fi

Monitoring and Health Checks

package main

import (
    "encoding/json"
    "net/http"
    "time"
)

type HealthStatus struct {
    Status    string    `json:"status"`
    Timestamp time.Time `json:"timestamp"`
    Version   string    `json:"version"`
    Uptime    string    `json:"uptime"`
}

var startTime = time.Now()

func healthHandler(w http.ResponseWriter, r *http.Request) {
    status := HealthStatus{
        Status:    "healthy",
        Timestamp: time.Now(),
        Version:   version, // Set at build time
        Uptime:    time.Since(startTime).String(),
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(status)
}

func readinessHandler(w http.ResponseWriter, r *http.Request) {
    // Check database connection, external services, etc.
    if isReady() {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ready"))
    } else {
        w.WriteHeader(http.StatusServiceUnavailable)
        w.Write([]byte("not ready"))
    }
}

func isReady() bool {
    // Implement your readiness checks
    // - Database connection
    // - External service availability
    // - Required resources loaded
    return true
}

func main() {
    http.HandleFunc("/health", healthHandler)
    http.HandleFunc("/ready", readinessHandler)
    
    http.ListenAndServe(":8080", nil)
}

Best Practices

  1. Use multi-stage Docker builds: Reduce image size
  2. Run as non-root user: Improve security
  3. Include health checks: For container orchestration
  4. Use environment variables: For configuration
  5. Implement graceful shutdown: Handle SIGTERM properly
  6. Log structured data: Use JSON logging
  7. Monitor resource usage: CPU, memory, disk
  8. Automate deployment: Use CI/CD pipelines
  9. Test deployments: Use staging environments
  10. Plan rollback strategies: Have backup plans

Go’s deployment story is excellent due to static binaries and simple runtime requirements. Choose the deployment strategy that best fits your infrastructure and operational needs.

For more on Docker, check our Docker tutorial. If you need to configure your application, see the configuration tutorial.

Last updated on