Despliegue de Aplicaciones Go en Linux

Go (Golang) es un lenguaje compilado que produce binarios nativos autocontenidos—sin dependencias externas de runtime—lo que simplifica enormemente el despliegue en producción. Un binario Go incluye todo lo necesario para ejecutarse, facilitando estrategias de despliegue sin gestores de paquetes ni entornos virtuales. Esta guía cubre la compilación de binarios Go para producción, la creación de servicios systemd, la configuración de Nginx como proxy inverso, el apagado gracioso y el despliegue con Docker.

Requisitos Previos

  • Ubuntu 20.04+ o Rocky Linux 8+
  • Go 1.20+ instalado (en la máquina de build)
  • Nginx instalado en el servidor
  • Acceso root o sudo
# Instalar Go en Ubuntu/Debian
wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
go version

# En CentOS/Rocky
dnf install golang

# Verificar instalación
go version
echo $GOPATH

Compilación para Producción

Aplicación Go de Ejemplo

// main.go - Servidor HTTP de ejemplo para producción
package main

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

type HealthResponse struct {
    Status  string `json:"status"`
    Version string `json:"version"`
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(HealthResponse{Status: "ok", Version: "1.0.0"})
}

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    mux := http.NewServeMux()
    mux.HandleFunc("/health", healthHandler)
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Aplicación Go en producción")
    })

    server := &http.Server{
        Addr:         ":" + port,
        Handler:      mux,
        ReadTimeout:  30 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    // Arrancar en goroutine para poder hacer apagado gracioso
    go func() {
        log.Printf("Servidor iniciado en puerto %s", port)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Error al iniciar servidor: %v", err)
        }
    }()

    // Esperar señal de apagado
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("Iniciando apagado gracioso...")
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Error en el apagado del servidor: %v", err)
    }
    log.Println("Servidor detenido correctamente")
}

Compilar el Binario

# Compilar para el sistema actual
go build -o mi-app main.go

# Compilar con optimizaciones para producción
# -ldflags="-s -w" elimina información de debug y reduce el tamaño
go build -ldflags="-s -w" -o mi-app main.go

# Ver el tamaño del binario
ls -lh mi-app

# Compilar con información de versión embebida
VERSION=$(git describe --tags --always 2>/dev/null || echo "v1.0.0")
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")

go build \
    -ldflags="-s -w -X main.Version=$VERSION -X main.BuildTime=$BUILD_TIME -X main.Commit=$COMMIT" \
    -o mi-app \
    main.go

echo "Binario compilado: $(./mi-app --version 2>/dev/null || ls -lh mi-app)"

Compilación Cruzada

Go facilita la compilación para diferentes plataformas desde una sola máquina:

# Compilar para diferentes arquitecturas y sistemas operativos

# Linux AMD64 (el más común para servidores)
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o mi-app-linux-amd64 main.go

# Linux ARM64 (para servidores ARM como Ampere)
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o mi-app-linux-arm64 main.go

# macOS AMD64 (para desarrollo en Mac Intel)
GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o mi-app-darwin-amd64 main.go

# Windows AMD64
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o mi-app-windows-amd64.exe main.go

# Script de compilación multi-plataforma
cat > scripts/build.sh << 'EOF'
#!/bin/bash
VERSION=${1:-"v1.0.0"}
BINARY="mi-app"

mkdir -p dist

platforms=("linux/amd64" "linux/arm64" "darwin/amd64")

for platform in "${platforms[@]}"; do
    GOOS=${platform%/*}
    GOARCH=${platform#*/}
    OUTPUT="dist/${BINARY}-${GOOS}-${GOARCH}"
    
    echo "Compilando para $GOOS/$GOARCH..."
    GOOS=$GOOS GOARCH=$GOARCH go build \
        -ldflags="-s -w -X main.Version=$VERSION" \
        -o "$OUTPUT" main.go
    
    echo "  -> $OUTPUT ($(du -sh $OUTPUT | cut -f1))"
done

echo "Compilación completada."
ls -lh dist/
EOF
chmod +x scripts/build.sh

Creación del Servicio systemd

# Crear usuario dedicado para la aplicación
useradd -r -s /sbin/nologin -d /opt/mi-app go-app
mkdir -p /opt/mi-app/{bin,config,logs}
chown -R go-app:go-app /opt/mi-app

# Copiar el binario
cp mi-app /opt/mi-app/bin/
chmod +x /opt/mi-app/bin/mi-app

# Crear archivo de variables de entorno
cat > /opt/mi-app/config/.env << 'EOF'
PORT=8080
DB_URL=postgres://usuario:clave@localhost:5432/db
LOG_LEVEL=info
ENVIRONMENT=production
EOF
chmod 600 /opt/mi-app/config/.env
chown go-app:go-app /opt/mi-app/config/.env

# Crear servicio systemd
cat > /etc/systemd/system/mi-app-go.service << 'EOF'
[Unit]
Description=Aplicación Go en producción
After=network.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=go-app
Group=go-app
WorkingDirectory=/opt/mi-app

# Variables de entorno
EnvironmentFile=/opt/mi-app/config/.env
Environment=GOMAXPROCS=4

# Comando de inicio
ExecStart=/opt/mi-app/bin/mi-app

# Apagado gracioso - enviar SIGTERM y esperar hasta 30s
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30s
SendSIGKILL=yes

# Reinicio automático
Restart=on-failure
RestartSec=5s
StartLimitInterval=60s
StartLimitBurst=3

# Logging a journal
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mi-app-go

# Seguridad
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/mi-app/logs

[Install]
WantedBy=multi-user.target
EOF

# Activar y arrancar el servicio
systemctl daemon-reload
systemctl enable --now mi-app-go.service
systemctl status mi-app-go.service

Nginx como Proxy Inverso

cat > /etc/nginx/sites-available/go-app << 'EOF'
# Upstream para la aplicación Go
upstream go_app {
    server 127.0.0.1:8080;
    keepalive 32;  # Mantener conexiones persistentes al backend
}

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

server {
    listen 443 ssl http2;
    server_name miapp.com www.miapp.com;

    ssl_certificate /etc/letsencrypt/live/miapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/miapp.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Logs
    access_log /var/log/nginx/go-app-access.log;
    error_log /var/log/nginx/go-app-error.log;

    location / {
        proxy_pass http://go_app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";  # Para keepalive HTTP/1.1
        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 ajustados para Go
        proxy_connect_timeout 10s;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
    }

    location /health {
        proxy_pass http://go_app;
        access_log off;  # No registrar health checks
    }
}
EOF

ln -s /etc/nginx/sites-available/go-app /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Apagado Gracioso

El manejo de señales es crítico para deployments sin downtime:

# Despliegue sin tiempo de inactividad con systemd
# 1. Copiar nuevo binario
cp mi-app-nuevo /opt/mi-app/bin/mi-app.new
chmod +x /opt/mi-app/bin/mi-app.new

# 2. Reemplazar el binario atómicamente
mv /opt/mi-app/bin/mi-app /opt/mi-app/bin/mi-app.old
mv /opt/mi-app/bin/mi-app.new /opt/mi-app/bin/mi-app

# 3. Reiniciar el servicio (systemd enviará SIGTERM, Go hará shutdown gracioso)
systemctl reload mi-app-go.service  # Envía SIGHUP si la app lo soporta
# O:
systemctl restart mi-app-go.service

# 4. Verificar que el nuevo binario está funcionando
curl http://localhost:8080/health

# 5. Limpiar binario anterior si todo va bien
rm /opt/mi-app/bin/mi-app.old

# Para rollback rápido si algo sale mal:
mv /opt/mi-app/bin/mi-app /opt/mi-app/bin/mi-app.failed
mv /opt/mi-app/bin/mi-app.old /opt/mi-app/bin/mi-app
systemctl restart mi-app-go.service

Despliegue con Docker

# Dockerfile multi-etapa (build + runtime mínimo)
cat > Dockerfile << 'EOF'
# Etapa de compilación
FROM golang:1.21-alpine AS builder
WORKDIR /app

# Copiar archivos de dependencias
COPY go.mod go.sum ./
RUN go mod download

# Copiar código fuente
COPY . .

# Compilar el binario estático
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w -extldflags '-static'" \
    -o mi-app \
    main.go

# Etapa de producción (imagen mínima)
FROM scratch
# Importar certificados SSL
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copiar el binario
COPY --from=builder /app/mi-app /mi-app

# Usuario no root (ID numérico, ya que no hay /etc/passwd en scratch)
USER 1001

EXPOSE 8080

ENTRYPOINT ["/mi-app"]
EOF

# O usar distroless para mayor compatibilidad
cat > Dockerfile.distroless << 'EOF'
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 mi-app main.go

FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/mi-app /mi-app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/mi-app"]
EOF

# Construir la imagen
docker build -t mi-app-go:latest .
docker build -t mi-app-go:v1.0.0 .

# Verificar el tamaño de la imagen
docker images mi-app-go

# Ejecutar en producción
docker run -d \
    --name mi-app-go \
    --restart unless-stopped \
    -p 8080:8080 \
    -e PORT=8080 \
    -e DB_URL=postgres://user:pass@host:5432/db \
    mi-app-go:v1.0.0

Monitoreo y Logging

# Exponer métricas de Prometheus desde la aplicación Go
# Añadir al código:
# import "github.com/prometheus/client_golang/prometheus/promhttp"
# http.Handle("/metrics", promhttp.Handler())

# Ver logs del servicio Go
journalctl -u mi-app-go.service -f
journalctl -u mi-app-go.service -n 100 --no-pager

# Ver métricas de pprof (si están habilitadas en el código)
# curl http://localhost:8080/debug/pprof/
# go tool pprof http://localhost:8080/debug/pprof/heap

# Monitorear el proceso Go
PID=$(systemctl show --property MainPID --value mi-app-go.service)
cat /proc/$PID/status | grep -E "VmRSS|VmSize|Threads"

Solución de Problemas

El servicio no inicia (error de permisos):

systemctl status mi-app-go.service
journalctl -u mi-app-go.service -n 20
# Verificar permisos del binario
ls -la /opt/mi-app/bin/mi-app
# Debe ser ejecutable y propiedad del usuario correcto
chown go-app:go-app /opt/mi-app/bin/mi-app
chmod +x /opt/mi-app/bin/mi-app

Error "address already in use":

# Ver qué proceso usa el puerto
ss -tlnp | grep 8080
fuser 8080/tcp
# Detener el proceso anterior
systemctl stop mi-app-go.service
fuser -k 8080/tcp

El binario se compila pero produce error en producción:

# Verificar que el binario es compatible con la arquitectura del servidor
file mi-app
# Linux AMD64: "ELF 64-bit LSB executable, x86-64"
# Verificar dependencias dinámicas (no debería tener si usas CGO_ENABLED=0)
ldd mi-app

La aplicación pierde conexiones durante el reinicio:

# Verificar que el código implementa http.Server.Shutdown correctamente
# Aumentar TimeoutStopSec en el servicio systemd
systemctl edit mi-app-go.service
# Añadir:
# [Service]
# TimeoutStopSec=60s

Conclusión

Go es un lenguaje ideal para aplicaciones de servidor de alta disponibilidad gracias a sus binarios autocontenidos, bajo consumo de memoria y excelente rendimiento bajo carga. El patrón de despliegue con binarios estáticos, systemd para gestión del servicio y Nginx como proxy inverso es sencillo, robusto y fácil de automatizar. La implementación correcta del apagado gracioso con http.Server.Shutdown es esencial para deployments sin tiempo de inactividad en entornos de producción.