|

Cómo desplegar Docker en Azure: caso real paso a paso

En pocas palabras: El desarrollador devshadd desplegó la misma imagen Docker (devshadd/notes-app:v1, Node.js 20 + PostgreSQL 16) primero local con docker compose y después en Azure Container Instances, usando variables de entorno para la conexión a la base de datos sin modificar el contenedor.

Un desarrollador llamado devshadd documentó paso a paso cómo llevó una app de notas hecha con Node.js 20, Express y PostgreSQL 16 desde su máquina local hasta Azure Container Instances (ACI), usando la misma imagen Docker en los dos entornos y resolviendo en el camino errores reales de conexión, timeouts de build y configuración de red. El resultado quedó publicado en Docker Hub como devshadd/notes-app:v1, con la app corriendo en vivo en Azure antes de bajarla para no pagar de más.

Desplegar Docker en Azure significa tomar una imagen de contenedor construida y probada en local, subirla a un registro (en este caso Docker Hub) y correrla en un servicio de Azure que la ejecute sin que vos administres servidores. Azure Container Instances es el servicio que usó este caso concreto: levanta contenedores por demanda, sin cluster de Kubernetes de por medio, ideal para pruebas rápidas pero con límites claros de persistencia. Vale la pena leer el log completo con ojo crítico, porque los cuatro tropiezos que aparecen ahí no son errores de este desarrollador en particular: son los mismos con los que se choca casi cualquiera que prueba ACI por primera vez.

En 30 segundos

  • La app corrió local con docker compose y después en Azure ACI con la misma imagen, cambiando solo variables de entorno.
  • La imagen final pesó 48.9MB y quedó publicada como devshadd/notes-app:v1 en Docker Hub.
  • El error ECONNREFUSED 172.18.0.2:5432 apareció porque depends_on en Docker Compose no espera a que Postgres esté listo, solo a que el contenedor arranque.
  • En ACI, DB_HOST pasó de db a localhost porque todos los contenedores del grupo comparten la misma red interna.
  • ACI resultó rápido para levantar y parecido a docker compose, pero es efímero: sin volumen persistente, borrar el grupo borra los datos, y no tiene HTTPS nativo.

¿Qué demostró este despliegue de Docker en Azure ACI?

Demostró que una imagen Docker armada en local, sin ningún ajuste de código, puede correr tal cual en Azure Container Instances si la configuración se maneja por variables de entorno. La app usó DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME y DB_SSL como puntos de configuración externa, y ese fue el único cambio real entre local y nube.

El stack completo fue Node.js 20 con Express de backend, Postgres 16 como base de datos, y Docker como empaquetado. Server.js crea la tabla de notas al arrancar y expone un endpoint /health que devuelve {"status":"ok"}, algo que sirvió como chequeo rápido en cada etapa del despliegue.

El repo original era de otro autor (lexszy-note), y devshadd lo clonó, lo corrió local, y después publicó su propia versión en un repo propio. Nada raro ahí: es exactamente el flujo que cualquiera hace cuando aprende a desplegar Docker en Azure copiando un ejemplo funcional. Lo interesante no es el repo en sí, sino que el log de errores quedó intacto, con horarios y mensajes reales, en vez de mostrar solo la versión pulida donde “todo funcionó a la primera”.

¿Cómo se pasa de docker compose local a Azure Container Instances?

desplegar docker en azure diagrama explicativo

El camino documentado fue: clonar el repo, levantar todo local con docker compose up -d --build, publicar la imagen en Docker Hub, registrar los proveedores de Azure necesarios, y recién ahí crear el contenedor en la nube con un archivo YAML. Cinco pasos, en ese orden, sin saltear ninguno.

Antes de tocar Azure hubo que verificar herramientas: git 2.52.0, Docker 29.8.1, docker compose v5.5.1 y Azure CLI 2.90.0. Un detalle que complicó el arranque: docker info falló con failed to connect to docker API at npipe:////./pipe/dockerDesktopLinuxEngine. La causa era simple pero molesta, Docker Desktop estaba instalado en %LOCALAPPDATA%\Programs\DockerDesktop y no en la ruta de Program Files donde uno esperaría buscarlo. Más contexto en conectarte por SSH a tu VM de Azure.

Con Docker Desktop arriba, docker compose up -d --build levantó los dos servicios (app y db). Ojo con esto: el primer build se cortó a los 120 segundos bajando las imágenes postgres:16-alpine y node:20-alpine. La solución fue reintentar con un timeout de 300 segundos, que terminó agregando 82 paquetes en 26 segundos sin drama.

Ya con la app corriendo local, el paso a Docker Hub fue directo: docker tag krisking2024/notes-app:v1 devshadd/notes-app:v1 y docker push devshadd/notes-app:v1. El primer push también dio timeout (net/http: timeout awaiting response headers), pero como el push es resumible, reintentarlo alcanzó. El digest final quedó registrado como sha256:be7d376595330de84a093765019acda4ec777df4064570499210f5c6818c442e, con la imagen pesando 48.9MB en el registro.

Del lado de Azure, antes de crear nada hubo que registrar tres proveedores: Microsoft.ContainerInstance, Microsoft.DBforPostgreSQL y Microsoft.Web. Cada uno pasó de estado “Registering” a “Registered” en aproximadamente un minuto. Recién ahí se creó el grupo de recursos con az group create --name notes-app-rg --location eastus y se desplegó el contenedor con az container create --resource-group notes-app-rg --file aci-notes.yaml.

¿Por qué falla la conexión a la base de datos con depends_on en Docker Compose?

depends_on en Docker Compose solo espera a que el contenedor de la base de datos arranque, no a que Postgres esté realmente aceptando conexiones. Ese matiz causó el error Database initialization failed: ECONNREFUSED 172.18.0.2:5432 en los logs de la app, apenas segundos después del docker compose up.

¿Y cómo se soluciona sin reescribir el docker-compose? Con un reinicio manual del servicio afectado. La fuente documentó exactamente eso: docker compose restart app, seguido de un Start-Sleep 8 en PowerShell para darle margen a Postgres, y después revisar logs de nuevo. El resultado pasó de error de conexión a Notes app running on port 3000 y Database initialized (notes table ready). Para más detalles técnicos, mirá crear la infraestructura con Terraform en Azure.

Es un problema conocido en cualquiera que haya orquestado contenedores con dependencias de base de datos: el contenedor “está arriba” mucho antes de que el motor de base de datos termine su inicialización interna. La diferencia entre “arrancó el contenedor” y “el servicio adentro ya responde” es justamente lo que depends_on no resuelve por sí solo, y es la razón por la que Postgres, MySQL o cualquier motor con arranque lento suele generar este mismo síntoma la primera vez que se dockeriza un proyecto. Un healthcheck en el docker-compose (con pg_isready, por ejemplo) sería la solución más prolija a largo plazo, porque hace que Compose espere una señal real de “listo” en vez de asumirla. El fix de reintentar con sleep funcionó acá, pero es un parche pensado para una prueba puntual, no algo para dejar en un pipeline que se repite todos los días.

¿Qué cambia en las variables de entorno al mover la app de local a ACI?

El cambio central fue DB_HOST: en docker-compose apuntaba a db, el nombre del servicio dentro de la red interna de Compose, pero en Azure Container Instances tuvo que pasar a localhost. La razón es que todos los contenedores dentro de un mismo grupo de ACI comparten la misma interfaz de red, así que no hay un nombre de servicio al que apuntar, solo localhost.

Ese detalle no es intuitivo si venís de Kubernetes o de Compose, donde cada servicio tiene su propio nombre de host resolvible. En ACI todo vive en la misma “caja de red”, literalmente, y es un cambio fácil de pasar por alto si copiás y pegás el mismo .env que usabas en local sin revisarlo línea por línea.

Para la contraseña de la base, el YAML de ACI usó secureValue en vez de un valor plano, así el password no queda expuesto al listar la configuración del contenedor. Y para asegurar que Postgres estuviera listo antes de que arrancara la app, el comando de inicio del contenedor fue ["/bin/sh", "-c", "sleep 20 && node server.js"], un sleep de 20 segundos como colchón antes de intentar conectar.

¿Azure Container Instances es apto para producción con base de datos?

No, al menos no en la configuración que documenta este caso. ACI es rápido para levantar y se siente parecido a correr docker compose, pero es efímero: si borrás el grupo de contenedores y lo volvés a crear, perdés todos los datos porque no hay volumen persistente configurado. Tampoco ofrece HTTPS de forma nativa, así que quedó expuesto por HTTP plano en el puerto 3000. Sobre eso hablamos en certificaciones de Azure según tu rol.

Para este experimento eso no fue un problema, era justamente el punto: probar que la misma imagen corre en dos entornos distintos. Pero si estás pensando en algo real con usuarios y datos que necesitás conservar, ACI solo no alcanza. Necesitarías sumar un servicio de base de datos gestionado (Azure Database for PostgreSQL, por ejemplo) en vez de correr Postgres dentro del mismo grupo efímero, y un proxy con TLS delante.

Ejemplo hipotético (ilustrativo, no basado en un caso real): imaginemos un equipo chico que arma un formulario interno para que un cliente cargue tickets de soporte durante una campaña de dos semanas. Si la data se puede volcar después a otro lado y no pasa nada si se pierde entre reinicios, ACI con Postgres en el mismo grupo alcanza y sobra: se levanta en minutos, se apaga cuando termina la campaña y no queda infraestructura pagando de más. Ahora pensemos en ese mismo formulario, pero para gestionar reclamos de garantía de una tienda que no puede perder un solo registro: ahí ACI con Postgres embebido ya no sirve, porque un simple reinicio del grupo -algo que puede pasar por un update de Azure o un error de configuración- borraría los reclamos sin aviso. En ese segundo escenario hace falta separar la base en un servicio gestionado aparte, justamente lo que este despliegue evitó a propósito por ser una prueba.

¿Cómo se traduce esto en criterios concretos antes de elegir ACI para un proyecto propio? Tres preguntas simples ayudan a decidir:

  • ¿Puedo perder los datos sin que sea grave? Si la respuesta es sí, ACI con Postgres en el mismo grupo es aceptable. Si es no, necesitás un servicio de base de datos separado y persistente desde el arranque.
  • ¿El tráfico es constante o por ráfagas cortas? ACI factura por tiempo de ejecución, así que tiene sentido para cargas breves o demos, no para algo que va a estar prendido 24/7 durante meses.
  • ¿Necesito HTTPS de entrada? Si sí, hay que sumar un proxy o un servicio con TLS delante, porque ACI expone el puerto tal cual, sin certificado.

Vale la comparación con hosting tradicional: si lo que buscás es algo estable para un proyecto chico sin tener que armar toda esta orquestación de contenedores, redes y providers, un VPS con Docker ya instalado en donweb.com resuelve el mismo caso de uso con menos piezas móviles y sin el riesgo de perder datos al reiniciar.

¿Qué errores concretos aparecieron durante el despliegue y cómo se resolvieron?

Cuatro errores puntuales quedaron documentados, cada uno con su fix aplicado en el momento.

  • Timeout de build a los 120 segundos: bajando postgres:16-alpine y node:20-alpine se cortó la descarga. Se resolvió reintentando con un timeout de 300 segundos.
  • Error de JSON en curl.exe desde PowerShell: curl.exe -X POST -d '{"title":...}' tiraba Unterminated string in JSON. La solución fue usar Invoke-RestMethod con un body armado vía ConvertTo-Json, mucho más nativo en PowerShell.
  • Timeout en docker push: el primer intento de subir la imagen a Docker Hub falló a mitad de camino. Como el push es resumible, reintentar terminó completándolo sin perder las capas ya subidas.
  • Conflicto al crear el contenedor en Azure: az container create devolvió MultipleErrorsOccurred Conflict RegistryErrorResponse apuntando a index.docker.io. Esperar 30 segundos y reintentar resolvió el conflicto, probablemente por un límite temporal de Docker Hub al tirar de la imagen.

¿Hay un patrón común en estos cuatro errores? Sí, y vale la pena remarcarlo porque no es casualidad: ninguno fue un bug de código propio, todos fueron cuestiones de timing y de red entre servicios externos (Docker Hub, registros de imágenes, PowerShell). Reintentar, con paciencia y algún segundo de espera, resolvió todos los casos. Si algo deja este log es que buena parte de “aprender Docker en la nube” es simplemente acostumbrarse a que las primeras corridas fallan por timeouts triviales, no por errores de configuración graves.

Errores comunes al desplegar Docker en Azure

  • Asumir que depends_on garantiza orden de disponibilidad: solo garantiza orden de arranque del contenedor, no que el servicio interno (Postgres, en este caso) ya acepte conexiones.
  • Copiar el DB_HOST de local a ACI sin cambiarlo: el nombre de servicio de Compose no existe en ACI, ahí la referencia correcta es localhost porque comparten red.
  • No usar secureValue para credenciales en el YAML de ACI: dejar la contraseña en texto plano en el archivo de definición queda expuesto en cualquier az container show.
  • Dar por sentado que ACI persiste datos: sin volumen montado, borrar el grupo de recursos borra la base entera. Para algo real hace falta un servicio de base de datos gestionado aparte.

Preguntas Frecuentes

¿Cómo se despliega un contenedor Docker en Azure?

Se construye la imagen local, se publica en un registro como Docker Hub, se registran los providers de Azure necesarios (Microsoft.ContainerInstance entre otros) y se crea el contenedor con az container create apuntando a esa imagen mediante un archivo YAML de definición.

¿Qué es Azure Container Instances y para qué sirve?

Azure Container Instances (ACI) es un servicio de Azure para correr contenedores individuales o grupos de contenedores sin administrar servidores ni clusters. Sirve para pruebas rápidas, demos y cargas de trabajo cortas, no para producción con datos que necesiten persistir. Tema relacionado: comparativa entre Microsoft y GitHub.

¿Por qué mi app Docker no puede conectarse a la base de datos Postgres?

Lo más común es que depends_on haya dejado arrancar la app antes de que Postgres termine su inicialización interna, generando un ECONNREFUSED. Reiniciar el contenedor de la app con unos segundos de espera de por medio suele resolverlo, aunque agregar un healthcheck real al servicio de base de datos evita depender de un reintento manual cada vez.

¿Azure Container Instances guarda los datos si borro el contenedor?

No, salvo que hayas configurado un volumen persistente explícito. Si borrás el grupo de contenedores sin volumen, la base de datos que corría adentro (y todos sus registros) desaparece por completo.

¿Cómo publico una imagen Docker en Docker Hub?

Con docker tag imagen-local usuario/nombre:tag para renombrarla con tu usuario de Docker Hub, y después docker push usuario/nombre:tag para subirla. Si el login pide contraseña y usás login social, hay que generar un token de acceso personal (PAT) desde la configuración de la cuenta y usarlo como contraseña.

Conclusión

Este caso deja algo concreto: desplegar Docker en Azure con ACI funciona bien como paso intermedio entre “lo probé en mi máquina” y “lo tengo corriendo en la nube”, siempre que sepas de antemano que es un entorno efímero. La misma imagen corrió en los dos lados, cambiando solo variables de entorno, que es exactamente la promesa de Docker cumplida en la práctica.

Lo que no resuelve ACI es la persistencia de datos ni el HTTPS, así que si tu próximo paso es algo con usuarios reales, vas a necesitar sumar una base de datos gestionada aparte y algo delante que maneje certificados. Antes de replicar este flujo, probá primero en local con docker compose, confirmá que el healthcheck del endpoint responde, y recién ahí subí la imagen a un registro para desplegarla en la nube. Ese orden evita la mitad de los dolores de cabeza que aparecieron acá.

Fuentes

Te puede interesar...