Deploy: Binario y Docker

Por: Artiko
gotemplhtmxdeploydocker

Una de las mayores ventajas del stack Go + Templ + HTMX es el deploy: un unico binario sin dependencias. No necesitas Node.js, npm, ni un runtime pesado. En este capitulo compilamos, dockerizamos y preparamos la app para produccion.

Compilar el Binario

Primero genera el codigo Templ y luego compila:

templ generate
go build -o todo-app .

Eso es todo. todo-app es un binario ejecutable autonomo. Puedes copiarlo a cualquier servidor y ejecutarlo:

./todo-app
# Servidor en http://localhost:8080

Para compilar para otro sistema operativo:

# Linux desde macOS
GOOS=linux GOARCH=amd64 go build -o todo-app .

# Windows
GOOS=windows GOARCH=amd64 go build -o todo-app.exe .

El Binario es Auto-Contenido

A diferencia de Node.js o Python, el binario de Go:

Pero hay un problema: los archivos estaticos (styles.css) estan fuera del binario. Solucion: embed.FS.

Embeber Archivos Estaticos

Go permite embeber archivos directamente en el binario con la directiva //go:embed:

package main

import "embed"

//go:embed static/*
var staticFS embed.FS

Actualizamos main.go para servir desde el filesystem embebido:

package main

import (
    "embed"
    "fmt"
    "io/fs"
    "log"
    "net/http"
    "os"
    "todo-app/handlers"
    "todo-app/models"
)

//go:embed static/*
var staticFS embed.FS

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

    store := models.NewTodoStore()
    h := handlers.NewTodoHandler(store)

    mux := http.NewServeMux()

    // Archivos estaticos embebidos
    staticSub, _ := fs.Sub(staticFS, "static")
    mux.Handle("GET /static/",
        http.StripPrefix("/static/",
            http.FileServer(http.FS(staticSub))))

    // Rutas
    mux.HandleFunc("GET /", h.Index)
    mux.HandleFunc("GET /todos", h.List)
    mux.HandleFunc("POST /todos", h.Create)
    mux.HandleFunc("GET /todos/search", h.Search)
    mux.HandleFunc("GET /todos/{id}/edit", h.EditForm)
    mux.HandleFunc("GET /todos/{id}/view", h.View)
    mux.HandleFunc("PUT /todos/{id}/toggle", h.Toggle)
    mux.HandleFunc("PUT /todos/{id}", h.Update)
    mux.HandleFunc("DELETE /todos/{id}", h.Delete)
    mux.HandleFunc("DELETE /todos/completed", h.ClearCompleted)

    fmt.Printf("Servidor en http://localhost:%s\n", port)
    log.Fatal(http.ListenAndServe(":"+port, mux))
}

Ahora el CSS viaja dentro del binario. Un solo archivo para desplegarlo todo.

Variables de Entorno

Configuracion basica via variables de entorno:

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

Para produccion puedes agregar mas:

VariableDefaultUso
PORT8080Puerto del servidor
LOG_LEVELinfoNivel de logging

Graceful Shutdown

En produccion necesitamos cerrar el servidor correctamente, esperando que los requests activos terminen:

func main() {
    // ... setup ...

    srv := &http.Server{
        Addr:    ":" + port,
        Handler: mux,
    }

    go func() {
        fmt.Printf("Servidor en http://localhost:%s\n", port)
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("Error: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    fmt.Println("Cerrando servidor...")
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Shutdown forzado: %v", err)
    }
    fmt.Println("Servidor cerrado")
}

Importar los paquetes necesarios:

import (
    "context"
    "os/signal"
    "syscall"
    "time"
)

Al recibir SIGINT o SIGTERM, el servidor espera hasta 10 segundos para que los requests activos terminen.

Health Check

Un endpoint simple para monitoreo:

mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("ok"))
})

Dockerfile Multi-Stage

Usamos multi-stage build para minimizar el tamano de la imagen:

# Stage 1: Build
FROM golang:1.23-alpine AS builder

RUN go install github.com/a-h/templ/cmd/templ@latest

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN templ generate
RUN CGO_ENABLED=0 GOOS=linux go build -o /todo-app .

# Stage 2: Runtime
FROM alpine:3.20

RUN apk --no-cache add ca-certificates
COPY --from=builder /todo-app /todo-app

EXPOSE 8080
ENV PORT=8080

ENTRYPOINT ["/todo-app"]

Tambien puedes usar scratch en lugar de alpine para una imagen aun mas pequena:

FROM scratch
COPY --from=builder /todo-app /todo-app
EXPOSE 8080
ENTRYPOINT ["/todo-app"]

Con scratch no hay shell ni paquetes, solo el binario. Ideal si no necesitas depurar dentro del contenedor.

docker-compose.yml

services:
  todo-app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - PORT=8080
    restart: unless-stopped

Ejecutar:

docker-compose up --build

Comparativa de Tamano

StackImagen DockerDependencias
Go + Templ + HTMX (alpine)~15 MB0
Go + Templ + HTMX (scratch)~8 MB0
Node.js + Express + React~300+ MBnode_modules
Python + Flask + Jinja~150+ MBvenv

El binario de Go con archivos embebidos produce imagenes Docker ordenes de magnitud mas pequenas.

Logging Basico

Para produccion, agrega logging estructurado:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

Usarlo en main:

log.Fatal(srv.ListenAndServe()) // con el handler wrapeado
// O aplicar al mux:
// Handler: loggingMiddleware(mux)

Resumen del Tutorial

A lo largo de 14 capitulos construimos una aplicacion web completa:

CapituloTema
1-4Fundamentos de Go para web
5-6Templates tipados con Templ
7-8Interactividad con HTMX
9-13Proyecto Todo List completo
14Deploy con Docker

Lo que logramos

Proximos pasos

El stack Go + Templ + HTMX demuestra que se puede construir web interactiva, rapida y mantenible sin la complejidad de los frameworks JavaScript modernos.


<— Capitulo 13: Estilos y UX | Volver al indice —>