--- title: "Cómo desplegar una aplicación Go en Ubuntu 24.04 con systemd" description: "Compila una aplicación Go, ejecútala como servicio systemd con apagado ordenado, publícala tras Nginx con HTTPS en Ubuntu 24.04 y actualízala sin cortes." category: "API y Microservicios" language: es source: https://cubepath.com/es/docs/api-y-microservicios/despliegue-de-aplicacion-go-en-linux updated: 2026-09-25 --- # Cómo desplegar una aplicación Go en Ubuntu 24.04 con systemd Go compila a un único binario estático sin dependencias de runtime, así que desplegarlo en producción consiste en copiar un archivo y gestionarlo bien. En este tutorial compilarás una pequeña API HTTP en Go con versión embebida y apagado ordenado, la ejecutarás como servicio systemd con un usuario sin privilegios, la publicarás detrás de Nginx con un certificado de Let's Encrypt y aprenderás a actualizarla y revertirla en segundos. 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`. - Un dominio con un registro A apuntando a la IP del servidor. En esta guía se usa `your_domain`; sustitúyelo por el tuyo. - Los puertos 80 y 443 accesibles desde Internet. ## Paso 1: Instalar Go El paquete `golang-go` de Ubuntu 24.04 trae Go 1.22, una versión ya sin soporte. Instala la versión estable actual desde el sitio oficial. Primero consulta cuál es: ```bash GO_VERSION=$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n1) echo "$GO_VERSION" ``` ```output go1.27.1 ``` Descarga el archivo para tu arquitectura (`amd64` en la mayoría de servidores, `arm64` en servidores ARM) y extráelo en `/usr/local/go`, borrando antes cualquier instalación previa: ```bash curl -fsSLO "https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz" sha256sum "${GO_VERSION}.linux-amd64.tar.gz" ``` Compara el hash con el que publica [go.dev/dl](https://go.dev/dl/) para ese archivo y, si coincide, instálalo: ```bash sudo rm -rf /usr/local/go sudo tar -C /usr/local -xzf "${GO_VERSION}.linux-amd64.tar.gz" ``` Añade Go al `PATH` de tu usuario y carga el cambio en la sesión actual: ```bash echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile source ~/.profile go version ``` ```output go version go1.27.1 linux/amd64 ``` > Nota: Go solo hace falta en la máquina donde compilas. Si compilas en tu portátil o en CI, el servidor de producción no necesita tener Go instalado. ## Paso 2: Crear la aplicación de ejemplo Crea un directorio para el proyecto e inicializa un módulo: ```bash mkdir -p ~/goapp && cd ~/goapp go mod init example.com/goapp ``` Crea el archivo principal: ```bash nano main.go ``` La aplicación lee la dirección de escucha de la variable `LISTEN_ADDR`, expone `/health` con la versión y, al recibir `SIGTERM` (lo que envía `systemctl stop`), deja de aceptar conexiones y espera a que terminen las peticiones en curso con `http.Server.Shutdown`: ```go package main import ( "context" "encoding/json" "errors" "flag" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" ) // version se sobrescribe al compilar con -ldflags "-X main.version=..." var version = "dev" func main() { showVersion := flag.Bool("version", false, "muestra la versión y sale") flag.Parse() if *showVersion { fmt.Println(version) return } addr := os.Getenv("LISTEN_ADDR") if addr == "" { addr = "127.0.0.1:8080" } mux := http.NewServeMux() mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok", "version": version}) }) mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hola desde Go") }) srv := &http.Server{ Addr: addr, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, } // ctx se cancela al recibir SIGINT o SIGTERM ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() go func() { log.Printf("escuchando en %s (versión %s)", addr, version) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("error del servidor: %v", err) } }() <-ctx.Done() log.Println("señal recibida, cerrando conexiones") shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("cierre forzado: %v", err) } log.Println("servidor detenido") } ``` Los timeouts del `http.Server` no son opcionales en producción: sin `ReadHeaderTimeout` un cliente lento puede mantener conexiones abiertas indefinidamente. La aplicación escucha solo en `127.0.0.1`, porque el tráfico público llegará a través de Nginx. ## Paso 3: Compilar el binario de producción Compila con la versión embebida. `-trimpath` elimina las rutas locales del binario y `-ldflags="-s -w"` quita la información de depuración para reducir el tamaño: ```bash CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=1.0.0" -o goapp . ``` `CGO_ENABLED=0` garantiza un binario totalmente estático, que funciona en cualquier distribución Linux sin depender de la versión de glibc. Compruébalo: ```bash file goapp ./goapp -version ``` ```output goapp: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ..., stripped 1.0.0 ``` Si compilas en otra máquina (un Mac o un runner de CI), indica el sistema y la arquitectura de destino y copia el binario al servidor: ```bash GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=1.0.0" -o goapp . scp goapp your_user@your_server_ip:~/goapp/ ``` Usa `GOARCH=arm64` si el servidor es ARM. ## Paso 4: Crear el usuario y los directorios del servicio La aplicación no debe ejecutarse como root ni poder modificar su propio binario. Crea un usuario de sistema sin shell ni directorio personal: ```bash sudo useradd --system --no-create-home --shell /usr/sbin/nologin goapp ``` Instala el binario en `/opt/goapp`, propiedad de root y solo ejecutable para el resto: ```bash sudo install -d -m 0755 /opt/goapp sudo install -m 0755 -o root -g root ~/goapp/goapp /opt/goapp/goapp ``` Guarda la configuración en un archivo de entorno legible solo por root y el grupo `goapp`, que es el lugar adecuado para cadenas de conexión y secretos: ```bash sudo install -d -m 0750 -o root -g goapp /etc/goapp sudo nano /etc/goapp/goapp.env ``` ```ini LISTEN_ADDR=127.0.0.1:8080 # DATABASE_URL=postgres://app:your_strong_password@127.0.0.1:5432/app ``` ```bash sudo chown root:goapp /etc/goapp/goapp.env sudo chmod 0640 /etc/goapp/goapp.env ``` ## Paso 5: Crear el servicio systemd systemd arranca la aplicación al iniciar el servidor, la reinicia si falla, recoge sus logs en el journal y le envía `SIGTERM` al pararla. Crea la unidad: ```bash sudo nano /etc/systemd/system/goapp.service ``` ```ini [Unit] Description=goapp API HTTP en Go After=network-online.target Wants=network-online.target StartLimitIntervalSec=60 StartLimitBurst=5 [Service] Type=simple User=goapp Group=goapp EnvironmentFile=/etc/goapp/goapp.env ExecStart=/opt/goapp/goapp Restart=on-failure RestartSec=2 # Apagado ordenado: SIGTERM y hasta 30 s antes de SIGKILL KillSignal=SIGTERM TimeoutStopSec=30 # Endurecimiento NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ProtectKernelTunables=true ProtectControlGroups=true RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX LimitNOFILE=65536 [Install] WantedBy=multi-user.target ``` `TimeoutStopSec=30` es mayor que los 20 segundos que la aplicación se concede para cerrar, de modo que systemd no la mata a mitad del apagado. `ProtectSystem=strict` monta todo el sistema de archivos en solo lectura para el proceso; si tu aplicación necesita escribir en algún directorio, añádelo con `StateDirectory=goapp` (crea `/var/lib/goapp`) o `ReadWritePaths=`. Recarga systemd, activa el servicio y comprueba su estado: ```bash sudo systemctl daemon-reload sudo systemctl enable --now goapp systemctl status goapp --no-pager ``` ```output ● goapp.service - goapp API HTTP en Go Loaded: loaded (/etc/systemd/system/goapp.service; enabled; preset: enabled) Active: active (running) since ... Main PID: 4121 (goapp) ``` Prueba la aplicación en local: ```bash curl -s http://127.0.0.1:8080/health ``` ```output {"status":"ok","version":"1.0.0"} ``` Para ver los logs, incluido el apagado ordenado tras un reinicio: ```bash sudo systemctl restart goapp journalctl -u goapp -n 5 --no-pager ``` ```output ... goapp[4121]: señal recibida, cerrando conexiones ... goapp[4121]: servidor detenido ... systemd[1]: Stopped goapp.service - goapp API HTTP en Go. ... systemd[1]: Started goapp.service - goapp API HTTP en Go. ... goapp[4180]: escuchando en 127.0.0.1:8080 (versión 1.0.0) ``` ## Paso 6: Configurar Nginx como proxy inverso Nginx se encarga del TLS, de los clientes lentos y de los logs de acceso, y reenvía las peticiones a la aplicación. Instálalo y abre el firewall: ```bash sudo apt update sudo apt install nginx sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw enable ``` Crea el sitio: ```bash sudo nano /etc/nginx/sites-available/goapp ``` ```nginx upstream goapp { server 127.0.0.1:8080; keepalive 16; } server { listen 80; listen [::]:80; server_name your_domain; location / { proxy_pass http://goapp; proxy_http_version 1.1; proxy_set_header Connection ""; 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; proxy_read_timeout 60s; } location = /health { proxy_pass http://goapp; access_log off; } } ``` `keepalive 16` junto con `proxy_http_version 1.1` y la cabecera `Connection` vacía reutiliza las conexiones hacia Go en lugar de abrir una por petición. Activa el sitio, desactiva el sitio por defecto y valida la configuración: ```bash sudo ln -s /etc/nginx/sites-available/goapp /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx ``` ```output nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` Comprueba que responde desde fuera: ```bash curl -s http://your_domain/health ``` ## Paso 7: Activar HTTPS con Let's Encrypt Instala Certbot con su plugin de Nginx y solicita el certificado. Certbot añade el bloque `listen 443 ssl`, las rutas del certificado y la redirección de HTTP a HTTPS en el archivo del sitio: ```bash sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d your_domain --redirect ``` La renovación automática queda programada con un timer de systemd. Compruébala: ```bash systemctl list-timers certbot.timer --no-pager sudo certbot renew --dry-run ``` Y verifica el acceso por HTTPS: ```bash curl -s https://your_domain/health ``` ```output {"status":"ok","version":"1.0.0"} ``` ## Paso 8: Actualizar la aplicación y volver atrás Como el binario es un único archivo, una actualización es: dejar la nueva versión junto a la actual, sustituirla con un `mv` (un renombrado atómico, el proceso en marcha no se ve afectado) y reiniciar. Compila la nueva versión en `~/goapp` con `-X main.version=1.1.0` y ejecuta: ```bash sudo cp -p /opt/goapp/goapp /opt/goapp/goapp.prev sudo install -m 0755 -o root -g root ~/goapp/goapp /opt/goapp/goapp.new sudo mv /opt/goapp/goapp.new /opt/goapp/goapp sudo systemctl restart goapp curl -s http://127.0.0.1:8080/health ``` ```output {"status":"ok","version":"1.1.0"} ``` Durante el reinicio, las peticiones en curso terminan gracias al apagado ordenado, pero hay una ventana de unos milisegundos en la que el puerto no acepta conexiones nuevas y Nginx devolvería un 502. Para eliminarla del todo necesitas dos instancias detrás del `upstream` y reiniciarlas de una en una. Si la nueva versión falla, vuelve a la anterior: ```bash sudo mv /opt/goapp/goapp.prev /opt/goapp/goapp sudo systemctl restart goapp ``` ## Solución de problemas **El servicio no arranca con `status=203/EXEC`.** systemd no puede ejecutar el binario: suele ser una arquitectura equivocada (binario `arm64` en un servidor `amd64`) o falta el permiso de ejecución. Revisa con `file /opt/goapp/goapp` y `ls -l /opt/goapp/goapp`. **`bind: address already in use`.** Otro proceso ocupa el puerto. Localízalo con: ```bash sudo ss -ltnp 'sport = :8080' ``` **Nginx devuelve 502 Bad Gateway.** La aplicación no está escuchando en la dirección del `upstream`. Comprueba `systemctl status goapp`, el valor de `LISTEN_ADDR` y el log de errores con `sudo tail -n 20 /var/log/nginx/error.log`. **El servicio tarda 30 segundos en pararse.** La aplicación no está gestionando `SIGTERM` y systemd acaba enviando `SIGKILL`. Verifica que usas `signal.NotifyContext` y `srv.Shutdown` como en el paso 2. **`permission denied` al escribir archivos.** Es `ProtectSystem=strict` funcionando. Añade `StateDirectory=goapp` a la unidad y escribe en `/var/lib/goapp`. ## Conclusión Tienes una aplicación Go compilada como binario estático con su versión embebida, ejecutándose como servicio systemd endurecido con un usuario propio, publicada con HTTPS a través de Nginx y con un procedimiento de actualización y rollback de tres comandos. Como siguientes pasos, puedes automatizar la compilación y la copia del binario desde tu CI, exponer métricas en `/metrics` con la librería `prometheus/client_golang` o ejecutar dos instancias con una unidad plantilla de systemd (`goapp@.service`) para actualizar sin ningún corte.