Deploy: Binario y Docker
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:
- No necesita runtime instalado en el servidor
- No tiene
node_modulesni dependencias externas - Incluye el servidor HTTP (la stdlib de Go)
- Arranca en milisegundos
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:
| Variable | Default | Uso |
|---|---|---|
PORT | 8080 | Puerto del servidor |
LOG_LEVEL | info | Nivel 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
| Stack | Imagen Docker | Dependencias |
|---|---|---|
| Go + Templ + HTMX (alpine) | ~15 MB | 0 |
| Go + Templ + HTMX (scratch) | ~8 MB | 0 |
| Node.js + Express + React | ~300+ MB | node_modules |
| Python + Flask + Jinja | ~150+ MB | venv |
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:
| Capitulo | Tema |
|---|---|
| 1-4 | Fundamentos de Go para web |
| 5-6 | Templates tipados con Templ |
| 7-8 | Interactividad con HTMX |
| 9-13 | Proyecto Todo List completo |
| 14 | Deploy con Docker |
Lo que logramos
- Backend en Go con la stdlib (sin frameworks)
- Templates tipados con Templ (errores en compilacion, no en runtime)
- Interactividad con HTMX (sin escribir JavaScript)
- Deploy simple: un binario de ~8 MB que incluye todo
Proximos pasos
- Agregar persistencia con SQLite o PostgreSQL
- Implementar autenticacion (sessions o JWT)
- Agregar tests para handlers y store
- Explorar WebSockets con HTMX para actualizaciones en tiempo real
- Usar Air para hot reload en desarrollo
El stack Go + Templ + HTMX demuestra que se puede construir web interactiva, rapida y mantenible sin la complejidad de los frameworks JavaScript modernos.