--- title: "Cómo desplegar un servidor gRPC en Go con Nginx y TLS" description: "Crea un servicio gRPC en Go con Protocol Buffers, ejecuta dos instancias con systemd y publícalas en Ubuntu 24.04 con Nginx, TLS y health checks estándar." category: "API y Microservicios" language: es source: https://cubepath.com/es/docs/api-y-microservicios/despliegue-y-configuracion-de-servidor-grpc updated: 2026-09-25 --- # Cómo desplegar un servidor gRPC en Go con Nginx y TLS gRPC es un framework de llamadas a procedimientos remotos que usa HTTP/2 como transporte y Protocol Buffers para definir los servicios y serializar los mensajes, lo que lo hace más eficiente y estricto que una API REST con JSON. En este tutorial definirás un servicio gRPC, generarás el código en Go, implementarás un servidor con health checks estándar y apagado ordenado, ejecutarás dos instancias con systemd y las publicarás detrás de Nginx, que termina el TLS con un certificado de Let's Encrypt y reparte las llamadas entre ellas. Todo sobre Ubuntu 24.04. ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM. - Un usuario no root con privilegios `sudo`. - Go instalado desde [go.dev/dl](https://go.dev/dl/) en `/usr/local/go`, con `/usr/local/go/bin` en el `PATH`. El paquete `golang-go` de Ubuntu 24.04 es demasiado antiguo para las versiones actuales de `grpc-go`. - Un dominio o subdominio con un registro A apuntando al servidor. En esta guía se usa `your_domain`. - Los puertos 80 y 443 accesibles desde Internet. ## Paso 1: Instalar protoc y los plugins de Go `protoc` es el compilador de Protocol Buffers; los plugins `protoc-gen-go` y `protoc-gen-go-grpc` generan, respectivamente, los tipos de los mensajes y el código cliente y servidor de gRPC. Instala el compilador desde los repositorios de Ubuntu: ```bash sudo apt update sudo apt install protobuf-compiler protoc --version ``` ```output libprotoc 3.21.12 ``` Instala los plugins y `grpcurl`, una herramienta de línea de comandos para llamar a servicios gRPC, con `go install`. Los binarios quedan en `~/go/bin`, que debes añadir al `PATH`: ```bash go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.profile source ~/.profile protoc-gen-go-grpc --version ``` ```output protoc-gen-go-grpc 1.6.2 ``` ## Paso 2: Definir el servicio en Protocol Buffers Crea el proyecto y el directorio del contrato. Versionar el paquete (`greeter.v1`) te permite publicar más adelante una `v2` incompatible sin romper a los clientes actuales: ```bash mkdir -p ~/greeter/proto/greeter/v1 && cd ~/greeter go mod init example.com/greeter nano proto/greeter/v1/greeter.proto ``` ```protobuf syntax = "proto3"; package greeter.v1; option go_package = "example.com/greeter/proto/greeter/v1;greeterv1"; service Greeter { // Llamada unaria: una petición, una respuesta rpc SayHello(HelloRequest) returns (HelloReply); // Streaming de servidor: una petición, varias respuestas rpc SayHelloStream(HelloRequest) returns (stream HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } ``` Genera el código Go. `paths=source_relative` deja los archivos generados junto al `.proto`: ```bash protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ proto/greeter/v1/greeter.proto ls proto/greeter/v1 ``` ```output greeter.pb.go greeter.proto greeter_grpc.pb.go ``` No edites los archivos `.pb.go` a mano: se regeneran cada vez que cambias el `.proto`. ## Paso 3: Implementar el servidor Crea el programa del servidor: ```bash mkdir -p cmd/server nano cmd/server/main.go ``` ```go package main import ( "context" "fmt" "log" "net" "os" "os/signal" "syscall" "time" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/keepalive" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" greeterv1 "example.com/greeter/proto/greeter/v1" ) type greeterServer struct { greeterv1.UnimplementedGreeterServer instance string } func (s *greeterServer) SayHello(ctx context.Context, req *greeterv1.HelloRequest) (*greeterv1.HelloReply, error) { if req.GetName() == "" { return nil, status.Error(codes.InvalidArgument, "name es obligatorio") } return &greeterv1.HelloReply{Message: fmt.Sprintf("Hola, %s (instancia %s)", req.GetName(), s.instance)}, nil } func (s *greeterServer) SayHelloStream(req *greeterv1.HelloRequest, stream grpc.ServerStreamingServer[greeterv1.HelloReply]) error { for i := 1; i <= 3; i++ { msg := fmt.Sprintf("Hola %d/3, %s", i, req.GetName()) if err := stream.Send(&greeterv1.HelloReply{Message: msg}); err != nil { return err } select { case <-stream.Context().Done(): return stream.Context().Err() case <-time.After(time.Second): } } return nil } func main() { addr := os.Getenv("LISTEN_ADDR") if addr == "" { addr = "127.0.0.1:50051" } lis, err := net.Listen("tcp", addr) if err != nil { log.Fatalf("no se puede escuchar en %s: %v", addr, err) } srv := grpc.NewServer( grpc.KeepaliveParams(keepalive.ServerParameters{ MaxConnectionIdle: 5 * time.Minute, Time: 1 * time.Minute, Timeout: 20 * time.Second, }), grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ MinTime: 20 * time.Second, PermitWithoutStream: true, }), ) greeterv1.RegisterGreeterServer(srv, &greeterServer{instance: addr}) // Servicio estándar de salud (grpc.health.v1.Health) healthSrv := health.NewServer() healthpb.RegisterHealthServer(srv, healthSrv) healthSrv.SetServingStatus("greeter.v1.Greeter", healthpb.HealthCheckResponse_SERVING) // Reflexión: permite a grpcurl descubrir los servicios sin el .proto reflection.Register(srv) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() go func() { log.Printf("gRPC escuchando en %s", addr) if err := srv.Serve(lis); err != nil { log.Fatalf("error del servidor: %v", err) } }() <-ctx.Done() log.Println("señal recibida, apagado ordenado") healthSrv.Shutdown() // pasa todos los servicios a NOT_SERVING done := make(chan struct{}) go func() { srv.GracefulStop() // espera a que terminen las llamadas en curso close(done) }() select { case <-done: case <-time.After(20 * time.Second): log.Println("tiempo agotado, cerrando conexiones") srv.Stop() } log.Println("servidor detenido") } ``` Puntos clave del servidor: - Incrustar `UnimplementedGreeterServer` mantiene la compatibilidad si añades métodos al `.proto`: los que no implementes devuelven `Unimplemented` en lugar de romper la compilación. - Los errores se devuelven con `status.Error` y un código gRPC (`InvalidArgument`, `NotFound`...), que el cliente recibe de forma estructurada. - El servicio de salud implementa el protocolo estándar `grpc.health.v1.Health`, que entienden balanceadores, Kubernetes y `grpcurl`. - Los parámetros de `keepalive` cierran conexiones inactivas y detectan clientes caídos; la política de aplicación rechaza clientes que envíen pings con demasiada frecuencia. - `GracefulStop` deja de aceptar llamadas nuevas y espera a las que están en curso, con un límite de 20 segundos antes de forzar el cierre. Descarga las dependencias y compila un binario estático: ```bash go mod tidy CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o greeter-server ./cmd/server ``` Pruébalo en primer plano: ```bash ./greeter-server ``` ```output 2026/09/25 10:12:03 gRPC escuchando en 127.0.0.1:50051 ``` Desde otra sesión SSH, lista los servicios con `grpcurl`. `-plaintext` indica que esta conexión local no usa TLS: ```bash grpcurl -plaintext 127.0.0.1:50051 list ``` ```output greeter.v1.Greeter grpc.health.v1.Health grpc.reflection.v1.ServerReflection grpc.reflection.v1alpha.ServerReflection ``` Llama al método unario: ```bash grpcurl -plaintext -d '{"name": "CubePath"}' 127.0.0.1:50051 greeter.v1.Greeter/SayHello ``` ```output { "message": "Hola, CubePath (instancia 127.0.0.1:50051)" } ``` Detén el servidor con `Ctrl+C`. ## Paso 4: Ejecutar dos instancias con systemd Instala el binario y crea un usuario de sistema para el servicio: ```bash sudo install -m 0755 -o root -g root greeter-server /usr/local/bin/greeter-server sudo useradd --system --no-create-home --shell /usr/sbin/nologin greeter ``` Una unidad plantilla de systemd (con `@` en el nombre) permite arrancar varias instancias del mismo servicio. El texto después de `@` llega a la unidad como `%i`, y aquí se usa como puerto: ```bash sudo nano /etc/systemd/system/greeter@.service ``` ```ini [Unit] Description=Servidor gRPC greeter (puerto %i) After=network-online.target Wants=network-online.target [Service] Type=simple User=greeter Group=greeter Environment=LISTEN_ADDR=127.0.0.1:%i ExecStart=/usr/local/bin/greeter-server Restart=on-failure RestartSec=2 KillSignal=SIGTERM TimeoutStopSec=30 NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true [Install] WantedBy=multi-user.target ``` Arranca las instancias en los puertos 50051 y 50052: ```bash sudo systemctl daemon-reload sudo systemctl enable --now greeter@50051 greeter@50052 systemctl list-units 'greeter@*' --no-pager ``` ```output UNIT LOAD ACTIVE SUB DESCRIPTION greeter@50051.service loaded active running Servidor gRPC greeter (puerto 50051) greeter@50052.service loaded active running Servidor gRPC greeter (puerto 50052) ``` Comprueba el estado de salud de cada instancia con el protocolo estándar: ```bash grpcurl -plaintext -d '{"service": "greeter.v1.Greeter"}' 127.0.0.1:50052 grpc.health.v1.Health/Check ``` ```output { "status": "SERVING" } ``` Este mismo comando, con código de salida distinto de cero si el servicio no responde `SERVING`, es el que puedes usar en tu sistema de monitorización. ## Paso 5: Obtener el certificado TLS Los clientes gRPC en Internet deben conectarse siempre por TLS. Nginx terminará el TLS con un certificado de Let's Encrypt. Instala Nginx y Certbot y abre el firewall: ```bash sudo apt install nginx certbot python3-certbot-nginx sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw enable ``` Crea un bloque para el puerto 80, que Certbot usa para validar el dominio y que redirige el resto del tráfico a HTTPS: ```bash sudo nano /etc/nginx/sites-available/greeter ``` ```nginx server { listen 80; listen [::]:80; server_name your_domain; location / { return 301 https://$host$request_uri; } } ``` Actívalo y solicita el certificado. Con `certonly`, Certbot obtiene el certificado sin modificar la configuración de Nginx: ```bash sudo ln -s /etc/nginx/sites-available/greeter /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/default sudo nginx -t && sudo systemctl reload nginx sudo certbot certonly --nginx -d your_domain ``` ```output Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem Key is saved at: /etc/letsencrypt/live/your_domain/privkey.pem ``` Para que Nginx cargue el certificado renovado sin intervención, añade un hook de despliegue: ```bash sudo sh -c 'printf "#!/bin/sh\nsystemctl reload nginx\n" > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh' sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh ``` ## Paso 6: Configurar Nginx como proxy gRPC Nginx habla gRPC con el módulo `ngx_http_grpc_module`, incluido en el paquete de Ubuntu. La directiva `grpc_pass` reenvía cada llamada a un backend del `upstream`, repartiéndolas por turnos entre las dos instancias. Añade el bloque HTTPS al mismo archivo del sitio: ```bash sudo nano /etc/nginx/sites-available/greeter ``` ```nginx upstream greeter_grpc { server 127.0.0.1:50051 max_fails=3 fail_timeout=10s; server 127.0.0.1:50052 max_fails=3 fail_timeout=10s; keepalive 16; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name your_domain; ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; access_log /var/log/nginx/greeter-access.log; error_log /var/log/nginx/greeter-error.log; location / { grpc_pass grpc://greeter_grpc; grpc_set_header X-Real-IP $remote_addr; # Streams largos: evita que Nginx corte la llamada grpc_read_timeout 1h; grpc_send_timeout 1h; # Si no puede conectar con un backend, prueba el otro grpc_next_upstream error timeout; } } ``` - `http2` en el `listen` es obligatorio: gRPC no funciona sobre HTTP/1.1. - `grpc_read_timeout` por defecto es de 60 segundos, demasiado poco para llamadas de streaming que duran más. - `max_fails` y `fail_timeout` retiran temporalmente un backend que no responde, y `grpc_next_upstream` pasa la llamada al otro backend cuando la conexión falla o expira. Nginx no reintenta una llamada que ya llegó a enviar al backend, porque las peticiones gRPC son `POST` y no se consideran idempotentes. Valida y recarga Nginx: ```bash sudo nginx -t sudo systemctl reload nginx ``` ## Paso 7: Probar el servicio a través de TLS Desde tu equipo (con `grpcurl` instalado) o desde el propio servidor, llama al servicio por el puerto 443. Sin `-plaintext`, `grpcurl` usa TLS y valida el certificado contra las CA del sistema: ```bash grpcurl -d '{"name": "CubePath"}' your_domain:443 greeter.v1.Greeter/SayHello grpcurl -d '{"name": "CubePath"}' your_domain:443 greeter.v1.Greeter/SayHello ``` ```output { "message": "Hola, CubePath (instancia 127.0.0.1:50051)" } { "message": "Hola, CubePath (instancia 127.0.0.1:50052)" } ``` Las respuestas alternan entre instancias, lo que confirma el reparto de carga. Prueba también el streaming de servidor, que devuelve un mensaje por segundo: ```bash grpcurl -d '{"name": "CubePath"}' your_domain:443 greeter.v1.Greeter/SayHelloStream ``` ```output { "message": "Hola 1/3, CubePath" } { "message": "Hola 2/3, CubePath" } { "message": "Hola 3/3, CubePath" } ``` Y la validación de errores: ```bash grpcurl -d '{}' your_domain:443 greeter.v1.Greeter/SayHello ``` ```output ERROR: Code: InvalidArgument Message: name es obligatorio ``` > Advertencia: la reflexión expone la lista completa de servicios y mensajes a cualquiera que llegue al puerto 443. Es cómoda para depurar, pero en un servicio público quita `reflection.Register(srv)` y distribuye el archivo `.proto` a tus clientes, que pueden usarlo con `grpcurl -proto`. ## Paso 8: Actualizar sin interrumpir el servicio Con dos instancias puedes desplegar una nueva versión reiniciándolas de una en una. Mientras una se reinicia, Nginx envía las llamadas a la otra: ```bash sudo install -m 0755 -o root -g root greeter-server /usr/local/bin/greeter-server.new sudo mv /usr/local/bin/greeter-server.new /usr/local/bin/greeter-server sudo systemctl restart greeter@50051 grpcurl -plaintext -d '{"service": "greeter.v1.Greeter"}' 127.0.0.1:50051 grpc.health.v1.Health/Check sudo systemctl restart greeter@50052 ``` Reinicia la segunda instancia solo después de que la primera responda `SERVING`. En el journal verás el apagado ordenado: ```bash journalctl -u greeter@50051 -n 4 --no-pager ``` ```output ... greeter-server[7102]: señal recibida, apagado ordenado ... greeter-server[7102]: servidor detenido ... systemd[1]: Started greeter@50051.service - Servidor gRPC greeter (puerto 50051). ... greeter-server[7240]: gRPC escuchando en 127.0.0.1:50051 ``` ## Solución de problemas **`grpcurl` devuelve `Failed to dial target host`.** Nginx no escucha en 443 o el firewall lo bloquea. Comprueba `sudo ss -ltnp 'sport = :443'` y `sudo ufw status`. **El cliente recibe `Unavailable` con un 502 de Nginx.** Ninguna instancia responde. Revisa `systemctl status 'greeter@*'` y `/var/log/nginx/greeter-error.log`. **Error `upstream sent too large http2 frame` en el log de Nginx.** El backend no habla HTTP/2 en texto plano: el `upstream` apunta a un puerto HTTP/1.1 o a un servidor gRPC con TLS. En el segundo caso usa `grpc_pass grpcs://` en lugar de `grpc://`. Recuerda también que para gRPC se usa `grpc_pass`, no `proxy_pass`. **Los streams se cortan justo al minuto.** Falta `grpc_read_timeout` en la `location`; el valor por defecto es de 60 segundos. **`ENHANCE_YOUR_CALM` y `too_many_pings` en los logs del cliente.** El cliente envía pings de keepalive más a menudo que el `MinTime` de 20 segundos de la política del servidor. Sube el intervalo en el cliente o baja `MinTime`. ## Conclusión Tienes un servicio gRPC en Go definido con Protocol Buffers, con health checks estándar y apagado ordenado, ejecutándose en dos instancias gestionadas por systemd y publicado con TLS a través de Nginx, que reparte las llamadas y retira los backends caídos. Como siguientes pasos, puedes generar clientes en otros lenguajes a partir del mismo `.proto`, añadir autenticación con un interceptor que valide un token en los metadatos de cada llamada, o validar tus archivos `.proto` y detectar cambios incompatibles con la herramienta `buf`.