Cloud Run no arranca por PORT: reproduce el fallo y corrígelo paso a paso

Reproduce en Docker un puerto incorrecto, corrige la escucha en 0.0.0.0 y verifica PORT con 8080 y 9090. Ejemplo descargable y capturas reales.

Has preparado una imagen, la aplicación funciona en tu ordenador y Cloud Run rechaza la nueva revisión con un error de arranque. El mensaje menciona PORT. ¿Hay que cambiar el Dockerfile, el código o el servicio?

Vamos a separar las tres cosas con un ejemplo pequeño. Primero lo haremos fallar, después veremos dónde está escuchando realmente y finalmente comprobaremos la corrección con dos puertos distintos.

La solución para el caso de este tutorial es: leer PORT al arrancar y escuchar en 0.0.0.0. Cambiar solamente EXPOSE no cambia el puerto del servidor.

Ilustración de una nube y dos contenedores: una conexión al puerto 3000 está desconectada y otra al 8080 aparece conectada.

Portada conceptual original generada con IA. Las capturas del recorrido proceden de pruebas reales; ninguna representa la consola de Google Cloud.

Qué se ha probado y qué necesitas

El descargable se ha construido y ejecutado en Docker Desktop 4.73.0, Engine 29.4.3, macOS 27.0.1. Las imágenes son Linux/amd64 y utilizan Node.js 24 sobre Alpine, con la imagen base fijada por su digest. La versión exacta de Node aparece en la aplicación.

El ejercicio está probado en contenedores locales. No se ha desplegado en Cloud Run durante la preparación. Los pasos que afectan al servicio cloud están contrastados con documentación oficial y se identifican como tales. Ver un HTTP 200 local es una comprobación útil, pero no certifica IAM, red, facturación ni el despliegue remoto.

Necesitas un motor Docker compatible ya instalado, un navegador y una terminal. No necesitas instalar Node.js en el ordenador ni ejecutar npm install: Node está dentro de la imagen. Para la primera construcción se descarga una imagen pública; las siguientes pueden aprovechar la caché.

El ejercicio local no usa una cuenta de Google, una tarjeta ni recursos cloud. Docker Desktop tiene sus propias condiciones de licencia: no es gratuito para todos los usos profesionales. Condiciones y requisitos oficiales.

La parte remota requiere un proyecto autorizado, permisos y la facturación que corresponda. Cloud Run, Artifact Registry y Logging tienen sus propias condiciones, consumos y posibles costes. No vamos a crear esos recursos en este recorrido local ni a prometer gratuidad. Precios de Cloud Run, Artifact Registry y Cloud Logging.

Descargar el ejemplo completo. Extrae el ZIP en una carpeta propia y abre una terminal dentro de ella, junto a Dockerfile. Los comandos están escritos en una sola línea; en PowerShell, utiliza curl.exe donde aparezca curl para evitar confundirlo con un alias.

1. Entender qué significa el error

La documentación de Cloud Run recoge este mensaje:

Container failed to start. Failed to start and then listen on the port defined by the PORT environment variable.

No significa que todos los casos se arreglen cambiando un número. También puede haber una excepción antes de escuchar, un comando de inicio incorrecto o una imagen incompatible. El mensaje nos dice que la revisión no ha quedado lista como esperaba el servicio. Diagnóstico oficial de arranque.

En un servicio Cloud Run, el contenedor de entrada debe escuchar en el puerto configurado. Cloud Run le proporciona ese valor mediante PORT; el valor predeterminado es 8080, pero puede cambiarse. La interfaz de escucha debe permitir que la petición llegue desde fuera del proceso: 0.0.0.0, en lugar de limitarse a 127.0.0.1. Los jobs tienen otro ciclo de vida y no son el objeto de este artículo. Contrato de ejecución.

2. Construir una aplicación que ignora PORT

El archivo server-roto.mjs hace deliberadamente esto:

import { start } from './app.mjs';
start(3000, '0.0.0.0');

Nuestro servidor escucha en 3000 aunque el entorno le indique 8080. La función start prepara una aplicación HTTP pequeña y registra el puerto que ha abierto de verdad. No consulta bases de datos ni servicios externos.

Construye la versión rota:

docker build --platform linux/amd64 --target roto -t davidcanton-port:roto .

Usamos linux/amd64 para generar una imagen que incluya la arquitectura requerida por Cloud Run, aunque el equipo de pruebas sea un Mac con Apple Silicon. La imagen base del Dockerfile está fijada para repetir el ejercicio; en un proyecto de producción habría que mantenerla y revisar actualizaciones.

3. Reproducir el desacuerdo entre puertos

Arranca este contenedor:

docker run -d --rm --name dc-port-roto -e PORT=8080 -p 127.0.0.1:19080:8080 -p 127.0.0.1:19081:3000 davidcanton-port:roto

¿Qué hemos pedido?

  • PORT=8080: el puerto que esperamos que use la aplicación.
  • 19080:8080: nuestro puerto local 19080 lleva al 8080 del contenedor.
  • 19081:3000: una segunda entrada de diagnóstico, para comprobar si el servidor está realmente en 3000.

Esos dos puertos del ordenador se publican solo en 127.0.0.1, para este ejercicio. Los números locales 19080 y 19081 no son puertos que debas copiar a la configuración de Cloud Run.

Abre http://127.0.0.1:19080/. La petición falla.

Captura real del navegador al acceder al puerto local 19080: el servidor no entrega una respuesta válida por el puerto esperado.

Error real de conexión al contenedor local. No es el mensaje de una revisión de Cloud Run. El texto del navegador puede variar entre sistemas.

4. Comprobar que el proceso sí está vivo

Ahora abre http://127.0.0.1:19081/. Esta dirección llega a 3000 dentro del mismo contenedor y responde.

Captura real de la aplicación: puerto de escucha 3000 y variable PORT recibida con valor 8080.

La aplicación está funcionando. El problema de este caso es dónde escucha: 3000 no coincide con el 8080 recibido.

Este segundo mapeo es una ayuda de diagnóstico del laboratorio. No lo añadimos a Cloud Run como un parche. En la nube debemos corregir la aplicación o alinear conscientemente el puerto del contenedor de entrada.

5. Leer los logs antes de cambiar más cosas

Consulta la salida del contenedor:

docker logs dc-port-roto

En nuestra prueba se registró esta línea real:

{"severity":"INFO","event":"listening","host":"0.0.0.0","port":3000,"configuredPort":"8080"}

Tenemos una hipótesis comprobable: el entorno proporciona 8080, pero la aplicación ha abierto 3000. No necesitamos aumentar permisos ni hacer público un servicio para arreglar ese desacuerdo.

Si el log mostrase una excepción antes de listening, habría que investigar esa excepción. Un error de importación o una variable obligatoria ausente no se corrigen con EXPOSE 8080.

6. Corregir la lectura del puerto

Abre server.mjs. Es la versión corregida incluida en el ZIP:

import { start } from './app.mjs';

const value = process.env.PORT ?? '8080';
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 65535) {
  console.error(JSON.stringify({
    severity: 'ERROR',
    event: 'invalid_port',
    message: 'PORT debe ser un entero entre 1 y 65535.'
  }));
  process.exit(1);
}
start(Number(value), '0.0.0.0');

El valor por defecto permite arrancar en local sin definir PORT. Si llega un valor inválido, preferimos un error explícito a escuchar en un puerto inesperado. La validación se ha probado también: el contenedor termina con código 1 y registra invalid_port.

Captura real del archivo server.mjs abierto como texto, con lectura de process.env.PORT, validación y escucha en 0.0.0.0.

Archivo real del descargable abierto en el navegador. No es una captura del editor de Cloud Run.

7. Revisar el Dockerfile sin confundir EXPOSE con el servidor

El mismo Dockerfile contiene tres objetivos: roto, loopback y corregido. Todos comparten la aplicación y la instrucción EXPOSE 8080; lo que cambia es el archivo de arranque.

EXPOSE documenta un puerto de la imagen. No modifica server.listen, no abre una escucha ni publica por sí solo ese puerto en tu ordenador. La prueba rota ya tenía EXPOSE 8080 y seguía escuchando en 3000. Referencia de Dockerfile.

La imagen solo copia los cuatro archivos de código necesarios, ejecuta el proceso como usuario node y no incluye claves, .env ni dependencias de un proyecto ajeno. La selección explícita del contexto se puede revisar en .dockerignore.

8. Construir y probar la versión corregida

docker build --platform linux/amd64 --target corregido -t davidcanton-port:corregido .
docker run -d --rm --name dc-port-ok -e PORT=8080 -p 127.0.0.1:19083:8080 davidcanton-port:corregido

Abre http://127.0.0.1:19083/.

Captura real de la versión corregida: HTTP 200, puerto real 8080, PORT recibido 8080 y dirección 0.0.0.0.

La respuesta la genera el proceso que está atendiendo la petición. El puerto mostrado procede de server.address().port, no de un texto fijo en la página.

9. Comprobarlo también sin la página de presentación

Pulsa Ver comprobación JSON, o ejecuta:

curl -i http://127.0.0.1:19083/healthz

Debe responder con estado 200. En el JSON, listeningPort será 8080, configuredPort será "8080" y listeningHost, "0.0.0.0".

Captura real del endpoint healthz con ok true, puerto 8080 y dirección de escucha 0.0.0.0.

El endpoint devuelve datos del servidor real. Este chequeo no sustituye comprobar la revisión cloud cuando se despliegue.

La guía oficial de prueba local propone comprobar la imagen antes de desplegar. En nuestro caso no hace falta montar credenciales: la aplicación no llama a ningún recurso de Google.

10. Demostrar que no hemos dejado 8080 fijo en otra parte

Prueba la misma imagen corregida, con otro valor:

docker run -d --rm --name dc-port-9090 -e PORT=9090 -p 127.0.0.1:19084:9090 davidcanton-port:corregido

Abre http://127.0.0.1:19084/.

Captura real de la misma imagen corregida ejecutada con PORT 9090: el servidor escucha en 9090.

Que funcione con 8080 y 9090 demuestra que el programa lee la configuración. El EXPOSE 8080 del Dockerfile no ha obligado al proceso a quedarse en 8080.

11. Probar el otro fallo: escuchar solo en localhost

server-loopback.mjs usa el puerto recibido, pero escucha en 127.0.0.1. Para probarlo:

docker build --platform linux/amd64 --target loopback -t davidcanton-port:loopback .
docker run -d --rm --name dc-port-loopback -e PORT=8080 -p 127.0.0.1:19082:8080 davidcanton-port:loopback

http://127.0.0.1:19082/ falla. Sin embargo, desde dentro del contenedor funciona:

docker exec dc-port-loopback node -e "fetch('http://127.0.0.1:8080/healthz').then(r=>r.json()).then(console.log)"

Esto también se ha probado. El puerto coincide, pero el servidor solo acepta conexiones desde su propio espacio de red. Corregir la dirección de escucha a 0.0.0.0 permite llegar desde el mapeo de Docker; en Cloud Run es parte del contrato del contenedor de entrada.

12. Dejar logs que ayuden a encontrar la causa

La aplicación escribe objetos JSON en la salida estándar. Un evento de arranque registra host, port y configuredPort; una petición registra método, ruta sin query string y estado. No vuelca variables de entorno completas, tokens ni cabeceras de autorización.

docker logs dc-port-ok

Captura real del archivo de logs extraído del contenedor corregido: evento listening en 8080 y una petición healthz con estado 200.

Salida real de docker logs, guardada sin reescribir sus líneas y abierta como archivo de texto. Es evidencia local, no la interfaz de Cloud Logging.

En Cloud Run, los logs de los contenedores se recogen desde stdout y stderr. Escribir JSON estructurado permite aportar campos útiles al diagnóstico. Para este ejemplo no hace falta instalar un SDK de Logging. Documentación oficial de logs.

13. Trasladar el diagnóstico a tu servicio Cloud Run

Esta sección es una guía documental para quien ya tenga un servicio y permisos autorizados. No se ha ejecutado sobre una cuenta cloud en esta preparación. No es un tutorial de alta de proyecto o de facturación; ese recorrido irá por separado.

Comprueba tres cosas en la revisión que falla: imagen exacta, comando de inicio y puerto del contenedor de entrada. Con gcloud, sustituye los nombres de ejemplo por los tuyos y especifica el proyecto y la región para evitar operar en otro destino:

gcloud run services describe MI_SERVICIO --project MI_PROYECTO --region MI_REGION --format export

La salida completa puede contener configuración interna. Revísala en tu equipo; no la pegues sin revisar en un chat público. En spec.template.spec.containers, contrasta la imagen, command/args si existen y ports.containerPort con el código de arranque de esa imagen.

Si tu aplicación lee PORT, configura el puerto del contenedor y deja que Cloud Run inyecte la variable. No añadas PORT como una variable personalizada en el formulario de variables de entorno. Puerto del servicio y variables reservadas.

Una opción, si el programa debe escuchar conscientemente en 3000, es configurar ese puerto en el servicio:

gcloud run services update MI_SERVICIO --port 3000 --project MI_PROYECTO --region MI_REGION

Ese comando modifica el servicio y crea una revisión. No lo ejecutes por probar en producción. En este ejemplo preferimos que el código respete el valor recibido, para poder usar la misma imagen con distintas configuraciones.

14. Revisar los logs de la revisión correcta

Una consulta de lectura documentada es:

gcloud run services logs read MI_SERVICIO --limit 30 --project MI_PROYECTO --region MI_REGION

Para delimitar una revisión en Logs Explorer, puedes empezar por este filtro:

resource.type="cloud_run_revision"
resource.labels.service_name="MI_SERVICIO"
resource.labels.revision_name="MI_REVISION"

Busca primero el error de arranque o la excepción original, no solo el aviso final de que la revisión no está lista. Tener un log listening que coincide con el puerto ayuda, pero también debes comprobar que la revisión acaba lista y atiende peticiones. Las consultas cloud se han contrastado documentalmente; no hay capturas inventadas de su resultado.

15. Validar una revisión antes de mover tráfico

Cuando tengas una imagen corregida en un registro autorizado, puedes preparar una revisión sin asignarle tráfico:

gcloud run deploy MI_SERVICIO --image MI_IMAGEN --port 8080 --no-traffic --tag prueba-port --project MI_PROYECTO --region MI_REGION

Este es un paso remoto que crea una revisión y puede generar consumo, no parte del laboratorio local ejecutado. Requiere revisar identidad de despliegue, identidad de ejecución, acceso al registro y políticas del proyecto. El artículo de publicación de imágenes cubrirá esa preparación por separado. No hace falta convertir el servicio en público para diagnosticar el puerto.

La etiqueta prueba-port permite obtener una URL de prueba para esa revisión. Comprueba que no estés reutilizando una etiqueta de tu equipo. Después verifica el estado de la revisión y esa URL, con autenticación cuando corresponda; la etiqueta no concede acceso público. No confundas que la revisión esté creada con que su aplicación responda correctamente. La URL principal puede seguir sirviendo la versión anterior si no has cambiado el tráfico. Despliegue de imágenes.

16. Conservar una vuelta atrás conocida

Si hay una revisión anterior comprobada, conserva su nombre y la distribución de tráfico actual antes de cambiarla. Para volver a enviar el 100 % a una revisión concreta, la documentación usa:

gcloud run services update-traffic MI_SERVICIO --to-revisions MI_REVISION_ANTERIOR=100 --project MI_PROYECTO --region MI_REGION

También modifica el servicio; no se ha ejecutado aquí. Si tenías un reparto deliberado entre varias revisiones, registra ese reparto en lugar de asumir que volver al 100 % es siempre la reversión adecuada. Tráfico y vuelta a una revisión.

17. Limpiar los contenedores del laboratorio

Detén únicamente los que has creado siguiendo estos pasos:

docker stop dc-port-roto dc-port-ok dc-port-9090 dc-port-loopback

Se lanzaron con --rm, de modo que Docker retira esos contenedores al detenerlos. Las imágenes quedan disponibles por si quieres repetir. No hace falta ejecutar una limpieza global de Docker ni borrar los contenedores de otros proyectos.

Si un puerto local estaba ocupado, cambia el número de la izquierda del mapeo y abre esa nueva dirección. Por ejemplo, 19086:8080 conserva 8080 dentro del contenedor y cambia solo la entrada del ordenador.

Una tabla para el próximo diagnóstico

Lo que observasQué comprobar
PORT=8080, pero el log dice 3000Código de arranque, comando efectivo y puerto real del proceso.
Escucha en 8080, pero solo en 127.0.0.1Dirección de escucha; probar desde dentro no equivale a poder llegar desde fuera.
Has cambiado EXPOSE y sigue fallandoEXPOSE no modifica el servidor ni publica puertos.
No aparece el evento de escuchaExcepción previa, dependencias, entrada del proceso o configuración obligatoria.
Funciona en ARM local, pero no arranca en Cloud RunQue la imagen disponible incluya Linux/amd64 y el programa sea compatible.
La revisión está lista, pero una petición devuelve 403Separar IAM/ingress/autenticación del problema de arranque; no asumir que es el mismo error.
Solo falla la nueva versiónComparar imagen, configuración y logs de esa revisión, sin mezclar versiones.

Descargable y límites de esta prueba

El ZIP del ejemplo incluye código, Dockerfile, instrucciones, comprobación automática y licencia MIT. El programa no instala dependencias npm ni incorpora claves.

Se han verificado cinco escenarios en contenedores Linux/amd64: puerto equivocado, dirección de escucha limitada, corrección con 8080, corrección con 9090 y valor por defecto sin PORT. También se ha probado el rechazo de un valor inválido, la respuesta JSON y logs reales. Las capturas se tomaron con Chrome 153 en una sesión aislada; se comprobó además una ventana de 390 píxeles, sin teléfono físico.

No se ha desplegado la imagen en Google Cloud, movido tráfico, concedido permisos ni probado la facturación remota. No se ha validado en Windows durante esta sesión. Eso queda separado de la reproducción local, que sí se ha ejecutado. Fuentes y versiones revisadas el 20 de octubre de 2026.

El objetivo del diagnóstico es sencillo: averiguar qué proceso escucha, en qué dirección y en qué puerto, antes de cambiar otras piezas.