Cómo desplegar una app Node.js con Docker y una base de datos, sin administrar un servidor

Un acortador de enlaces con un Dockerfile, subido y en producción con base de datos en 42 segundos, sin aprovisionar un servidor. Medido paso a paso, incluidas las cuatro cosas que salieron mal.

Un Dockerfile de 8 líneas se convierte en un container en ejecución en Back4app, que guarda sus objetos Link en un backend de Back4app — del push a producción, medido en 42 segundos

Traducción del original en inglés, probado el 10 de septiembre de 2026. El código, los números y las fechas son los del original. Read in English

Tu app corre en localhost. Tiene un Dockerfile. Y cada página de hosting que abres responde a la pregunta “¿dónde corre mi código?”, pero no a la que de verdad te quita el sueño: “¿dónde viven mis datos y quién parchea ese servidor?”

Este post trata el caso corriente: una app web en Node.js, Express sirviendo una página y dos rutas, que necesita un sitio donde correr y un sitio donde guardar sus datos. Una plataforma de containers ejecuta tu código. Un backend te da la base de datos, las reglas y la autenticación que no escribiste. La mayoría de las apps necesitan las dos cosas; pocas plataformas ofrecen las dos.

La misma forma tiene otros tres posts, uno por variante: una API en Python con FastAPI, una app Next.js renderizada en el servidor y un worker que nunca debe dormirse.

Construimos un acortador de enlaces (42 líneas de Express, un Dockerfile de 8 líneas), hicimos push y lo tuvimos guardando datos detrás de HTTPS sin un servidor que administrar. El primer deploy tardó 42 segundos del clic al 200. Los redeploys tardaron entre 14 y 33 segundos. Todo lo que sigue sale de esa ejecución, del 10 de septiembre de 2026, incluidas las partes que fallaron.

Consigue el código funcionando: el repositorio companion está en github.com/templates-back4app/link-shortener — server.js, Dockerfile, cloud/main.js y deploy-check.sh, que verifica un despliegue de punta a punta. Clónalo, dale una estrella o abre un issue cuando algo se desvíe.

¿Qué incluye “sin administrar un servidor”?

Dilo en voz alta y suena a una sola cosa. Son cinco. Una plataforma solo puede afirmarlo con honestidad si cubre la lista entera:

Lo que necesitasPor qué no es opcionalDónde está en este post
Tu código en ejecuciónLa parte obviaContainer, sección 03
HTTPS en una URL públicaLos navegadores y las tiendas de apps rechazan el HTTP planoContainer, automático
Una base de datos que no administrasLos datos sobreviven al containerBackend, sección 04
Validación y autenticación fuera del containerEl container es desechable; las reglas noBackend, sección 05
Redeploy en cada cambio, sin downtimeSi no, vuelves al SSHSección 06

La mayoría de los hosts de containers se quedan en las dos primeras: ejecutan el Dockerfile y te dan una URL. La base de datos es un servicio aparte, facturado aparte, y la autenticación la escribes tú.

Back4app es la que probamos porque ofrece la segunda mitad como producto: una app de backend con base de datos gestionada, autenticación, REST y Cloud Code, con la que una app de container habla por la red. Dos apps, una cuenta, y el resto de este post es el recorrido medido por las dos.

El container es la única pieza para la que construyes una imagen. Todo lo que queda a su derecha es gestionado.

¿Cómo es el acortador de enlaces, archivo por archivo?

Un acortador de enlaces: la app más pequeña que ejercita las dos mitades. El container sirve una página y dos rutas: POST /shorten crea un objeto Link en el backend, GET /:code lo busca y redirige. El backend es el dueño de los datos y de las reglas.

Arquitectura: el navegador llama a tu container en b4a.run; el container lee y escribe objetos Link en el backend de Back4app a través del SDK; un hook beforeSave de Cloud Code valida cada Link antes de que llegue a la base de datos gestionada

Cuatro archivos importan. El Dockerfile completo:

# Stack: Docker | Node.js 22 (alpine) | File: Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 8080
CMD ["node", "server.js"]

El container lee su configuración del entorno; nada más va horneado en la imagen:

// Stack: Node.js 22.x | Express 5.x + Parse JS SDK 8.x | File: server.js (excerpt)
const { PORT = 8080, PARSE_APP_ID, PARSE_JS_KEY,
        PARSE_SERVER_URL = "https://parseapi.back4app.com" } = process.env;

Parse.initialize(PARSE_APP_ID, PARSE_JS_KEY);
Parse.serverURL = PARSE_SERVER_URL;

app.post("/shorten", async (req, res) => {
  const link = new Parse.Object("Link");
  link.set("url", String(req.body.url ?? ""));
  const saved = await link.save(); // the backend validates and assigns the code
  const short = `https://${req.get("host")}/${saved.get("code")}`;
  res.status(201).json({ code: saved.get("code"), short, url: saved.get("url") });
});
Medido · acortador de enlaces · septiembre de 2026
42líneas en el container
→
19líneas de Cloud Code
0servidores que parchear

Necesitas una cuenta gratuita de Back4app, una cuenta de GitHub (el container se despliega desde un repositorio) y curl. Sin Docker en tu máquina: todo este post se produjo en un Mac sin Docker instalado, porque la imagen la construye la plataforma.

¿Cómo se despliega un Dockerfile de Node.js desde GitHub?

Antes tienen que existir dos cosas: el backend con el que hablará el container y un repositorio de GitHub que la plataforma pueda leer. Hicimos primero el backend para tener sus claves listas.

Backend. Dashboard → New App → Build your Backend, ponle nombre, Create. Ochenta segundos después, la página Overview muestra el App ID y, en el desplegable Keys, la JavaScript key: los dos valores que necesita el container.

La página Overview del backend link-shortener: el App ID y el desplegable Keys a la izquierda, Parse Server 7.5.2 y MongoDB 3.6 a la derecha

Repositorio. Sube los cuatro archivos a GitHub. Después, en el lado de Containers, New App → Deploy from GitHub → Import GitHub Repo. Esto abre la página de instalación en GitHub de la app Back4App Containers: elige Only select repositories, selecciona el repo, Install & Authorize. Dieciséis segundos después de ese clic, el repo aparecía listado en la dashboard.

La pantalla Deploy a web app listando el repositorio link-shortener con un botón Select, después de instalar la GitHub App

Configurar y desplegar. Selecciona el repo y el formulario pide un nombre, la rama y un método de build: detectó el Dockerfile. Dejamos el puerto vacío (la plataforma lee EXPOSE), pusimos el health check en /healthz y añadimos dos variables de entorno: PARSE_APP_ID y PARSE_JS_KEY. Después, Deploy.

Hicimos clic a las 11:12:45. El log fue PREPARING → FETCHING GITHUB REPOSITORY → BUILDING IMAGE → Pushed image → LAUNCHING CONTAINER → CHECKING HEALTH → DEPLOYMENT READY, y /healthz respondió 200 a las 11:13:27.

El Overview del container tras el primer deploy: estado Available, build Ready en 29 s, el log de despliegue terminando en DEPLOYMENT READY y el panel de configuración mostrando Dockerfile, puerto auto, 2 variables y health check /healthz

Medido · primer deploy · 10 de septiembre de 2026
42 sclic en Deploy → primer 200
→
29 sde ellos, build de la imagen
64,6 MBimagen comprimida · 8 capas

Cómo lo medimos: un cronómetro desde el clic en Deploy hasta el primer 200 de la URL pública, en el plan Free (0,25 vCPU, 256 MB), un deploy el 10 de septiembre de 2026. Los tiempos de redeploy son cuatro pushes separados el mismo día.

Tu tiempo de build se mueve con el tamaño de la imagen y la velocidad del registry; los números de aquí son una referencia, no una promesa.

La URL es https://linkshortener-<id>.b4a.run: HTTPS incluido, sin certificado que solicitar. El container gratuito recibe 0,25 vCPU, 256 MB y 100 GB de transferencia.

La página del acortador de enlaces desplegado, servida desde linkshortener-awjh19o8.b4a.run: un campo de URL y un botón Shorten

¿Cómo guardas datos sin una base de datos?

Ya lo hiciste, en el código de arriba: link.save() envía el objeto al backend por HTTPS, con el App ID y la clave del entorno. No hay connection string, ni pool, ni proceso de base de datos dentro del container.

Aquí está la misma escritura de dos formas. El container usa el SDK; la forma REST es lo que el SDK envía, y funciona desde cualquier sitio, incluida tu terminal, con tus propias claves:

// Stack: Node.js 22.x | Parse JS SDK 8.x | File: server.js (the write)
const link = new Parse.Object("Link");
link.set("url", "https://www.back4app.com/docs/");
const saved = await link.save();
console.log(saved.get("code")); // assigned by the backend hook, e.g. "zASPnB"

Misma escritura, mismo backend. La forma REST es lo que el SDK envía por debajo.

El primer save desde el container creó la clase Link por sí solo: no se definió ningún schema de antemano (Parse Server 7.5.2 en Back4app, septiembre de 2026). Segundos después los objetos eran visibles en el Database Browser de la dashboard, con las columnas url, code y clicks inferidas de lo que escribió el container.

El Database Browser mostrando la clase Link con tres objetos y sus columnas url, clicks y code

Latencia, medida desde un portátil en UTC−3 con 10 muestras cada una: una escritura que pasa por el container hasta el backend tardó una mediana de 205 ms de punta a punta; la misma escritura enviada directamente al backend tardó 440 ms. El container y el backend están en la misma región (USA East), así que el salto entre ellos es la parte barata del viaje.

¿Dónde viven la validación y la generación del código corto?

Todo lo que tiene que sobrevivir a que el container sea reemplazado: las reglas sobre tus datos, las claves y los usuarios. En nuestra primera ejecución el backend aceptó un Link cuya url era la cadena not a url, y no devolvió ningún código corto, porque nadie le había dicho qué es un Link válido. Ese es el momento de añadir el hook.

cloud/main.js corre dentro del backend, no en el container. Rechaza las URLs malas, pone clicks a 0 y asigna un código que ningún otro Link use:

// Stack: Node.js 22.x | Parse Server 8.x | File: cloud/main.js
const ALPHABET = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
const code = () => Array.from({ length: 6 }, () => ALPHABET[Math.floor(Math.random() * ALPHABET.length)]).join("");

Parse.Cloud.beforeSave("Link", async (request) => {
  const link = request.object;
  if (!link.isNew()) return; // updates (click counts) pass through untouched

  let url;
  try { url = new URL(link.get("url")); } catch { url = null; }
  if (!url || !["http:", "https:"].includes(url.protocol)) {
    throw new Parse.Error(Parse.Error.VALIDATION_ERROR, "url must be an http(s) URL.");
  }
  link.set("url", url.toString());
  link.set("clicks", 0);

  // assign a short code that no other Link uses
  for (let attempt = 0; attempt < 5; attempt++) {
    const candidate = code();
    const taken = await new Parse.Query("Link").equalTo("code", candidate).first({ useMasterKey: true });
    if (!taken) { link.set("code", candidate); return; }
  }
  throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, "could not allocate a short code.");
});

Pégalo en Cloud Code → cloud/main.js en la dashboard del backend y Deploy. Nuestro hook respondía 21 segundos después del clic. Desde entonces el POST /shorten del container devolvió un code, y not a url volvió como 400 {"code":142,"error":"url must be an http(s) URL."}; el código del container no cambió en absoluto.

El editor de Cloud Code en la dashboard del backend con cloud/main.js abierto, mostrando el hook beforeSave

La misma regla vale para las claves y los usuarios. El container tiene la clave de JavaScript, que es una clave de cliente: puede hacer exactamente lo que podría hacer un navegador. La master key nunca sale del backend, y la clase User de Parse, las sesiones y las ACL están ahí para cuando el acortador tenga propietarios.

¿Qué pasa cuando vuelves a desplegar?

El auto deploy está desactivado en los containers gratuitos (el interruptor aparece deshabilitado), así que un git push solo no cambia nada en la URL en vivo. Hicimos push de un cambio de una línea a las 11:26 y el sitio siguió sirviendo la versión antigua hasta que desplegamos a mano.

Deploy latest commit no despliega de inmediato. Primero abre una revisión Finish your setup: la plataforma había leído el repo como “Docker / Node.js / Express · Node.js 22 Alpine” y sugería node server.js y el puerto 8080. Nada es obligatorio; Deploy without changes inicia el build.

El diálogo Finish your setup: la plataforma leyó el repo como Docker / Node.js / Express en Node.js 22 Alpine, sugiere el comando de inicio node server.js y el puerto 8080, con los botones Deploy without changes y Save and deploy

Medimos cuatro redeploys. Un build con caché y un cambio de una línea pasó del clic a la versión nueva respondiendo en 14 s; los otros tardaron 20, 32 y 20 s. Durante cada uno de ellos, una solicitud cada cinco segundos nunca vio otra cosa que 200: la lista de deployments muestra el container antiguo como Destroying solo después de que el nuevo está Ready.

La lista de Deployments tras varios redeploys: el deployment más reciente Ready en 20 s, el anterior Destroying, los más antiguos Destroyed

Las variables de entorno reciben el mismo trato: cambia una en Settings → Environment y la dashboard dice “Settings saved — deploy now with the settings you just saved?”. Una rotación de claves es un redeploy, no una sesión de SSH.

La página de configuración de variables de entorno listando PARSE_APP_ID y PARSE_JS_KEY, con la nota de que un cambio se aplica en el siguiente deploy

¿Qué errores dio el deploy de Node.js y qué los arregló?

Rompimos el despliegue a propósito tres veces. Dos de las tres no se comportaron como suelen prometer las guías de errores.

App is not listening on port 8080, but on port 3000 — switching to it (auto-detected). Pusimos PORT=3000 para que la app ignorara la línea EXPOSE 8080. Esperado: un health check fallido. Real: la plataforma sondeó, encontró el 3000, registró la línea de arriba, guardó el 3000 en la configuración de la app e informó DEPLOYMENT READY en 20 s. Quitar la variable lo devolvió al 8080 de la misma manera. Aquí, un puerto que no coincide no es un fallo.

El log de despliegue con las líneas App is not listening on port 8080, but on port 3000 — switching to it (auto-detected) y Port 3000 saved to the app settings for future deploys, seguidas de DEPLOYMENT READY

{"error":"unauthorized"} con HTTP 502 en /shorten, pero el deploy decía Ready. Desplegamos con una PARSE_JS_KEY incorrecta. El health check llama a /healthz, que nunca toca el backend, así que el container roto pasó y se puso en vivo; el fallo solo apareció en la primera solicitud real. Corrige la clave en Settings → Environment y despliega. Mejor aún: haz que tu health check ejercite el backend.

Enlaces cortos http://… aunque el sitio es HTTPS. La plataforma termina el TLS y, a septiembre de 2026, no reenvía el esquema, así que el req.protocol de Express es http incluso con trust proxy activado. Construye las URLs públicas con https:// de forma explícita, como ahora hace server.js.

Runtime Logs no muestra nada después de un fallo. Runtime Logs muestra el stdout y el stderr de tu container, etiquetados con el id del container: las líneas de console.log como SYSTEM, las de console.error como ERROR. Nuestra primera versión se tragó el error unauthorized y no dejó rastro. Registra tus fallos; entonces la página de logs es el primer sitio donde mirar.

Runtime Logs mostrando link-shortener escuchando en el 8080 y una línea ERROR shorten failed: 142 url must be an http(s) URL., cada una etiquetada con el id del container

Website blocked — known phishing site al abrir tu URL de b4a.run. Nuestra URL recién creada fue bloqueada por Threat Protection de NordVPN en Chrome, mientras curl llegaba sin problema. El dominio compartido carga con la reputación de todas las apps que viven en él. Verifica primero con curl -I y ponle un dominio propio a cualquier cosa que muestres a usuarios.

Nothing here, yet! después de instalar la GitHub App. El callback de autorización aterrizó en un navegador que no tenía la sesión iniciada en la dashboard. Desinstala la app en GitHub (Settings → Applications) y repite Import GitHub Repo desde la dashboard en el mismo navegador.

Success on deploying your changes! pero Cloud Code no hace nada. Revisa Logs → System buscando main.js not found; haz la edición en el propio editor para que aparezca “Files pending deploy” y despliega otra vez.

¿Cuándo es un container la respuesta equivocada?

Según nuestro propio manifiesto, las comparaciones en las que solo gana la plataforma no se publican, así que aquí está la otra columna.

  • Tu app es CRUD y nada más. Entonces no necesitas ningún container: la capa REST del backend ya es la API. Nuestro post sobre la REST API construye una en 24 líneas y cero containers.
  • Necesitas un motor de base de datos o una extensión concretos. El backend es Parse Server sobre MongoDB (Postgres en los planes de pago). Si tu app es PostGIS o ClickHouse, la mitad de datos de este post no aplica.
  • Necesitas que la URL dure más de una hora, auto deploy o más de 0,25 vCPU. Las tres son funciones de pago aquí a 15 de septiembre de 2026 (los planes Shared empiezan en US$ 5 al mes); consulta la página de precios, y no este post, para los números actuales.
  • Quieres construir las imágenes en local y subirlas. La vía de imagen acepta solo Docker Hub: nuestra imagen en GitHub Container Registry fue rechazada en el formulario.

Si ninguno de esos casos es el tuyo, la forma del diagrama (un container desechable delante de un backend gestionado) es la que elegiríamos otra vez.

¿Hacia dónde llevas el acortador de enlaces desde aquí?

Tienes un container que se vuelve a desplegar en menos de medio minuto, una base de datos que nunca aprovisionaste y una validación que sobrevive a que el container sea reemplazado. Desde aquí:

  • Dales propietarios a los enlaces. La clase User de Parse añade endpoints de creación de cuenta e inicio de sesión sin código en el container (medido, con restablecimiento de contraseña); un beforeSave puede estampar el usuario creador en cada Link y una ACL que solo él pueda editar (cómo se comportan los cerrojos).
  • Ponle un dominio. Settings → Domains en el container: la solución al problema de reputación de arriba.
  • Haz honesto el health check. Haz que /healthz ejecute una query barata contra el backend, para que una clave mala haga fallar el deploy y no al primer usuario.

Todo el código de arriba está completo y probado: el repositorio companion incluye deploy-check.sh, que ejecuta health → shorten → redirect contra cualquier URL y sale con código distinto de cero si algo falla.

Etiquetasdockercontainersnodeexpressparse-server

Preguntas frecuentes

¿Puedo desplegar un container de Docker sin administrar un servidor?

Sí. En Back4app Containers conectas un repositorio de GitHub con un Dockerfile; la plataforma construye la imagen (kaniko), ejecuta el container, termina el HTTPS y te entrega una URL. En nuestra ejecución medida, el primer deploy fue del clic en Deploy a una respuesta 200 en 42 segundos (septiembre de 2026).

¿El plan gratuito incluye una base de datos?

El plan del container en sí no: te da 0,25 vCPU, 256 MB y 100 GB de transferencia y, desde el 15 de septiembre de 2026, una URL que vive 60 minutos por deploy; una URL permanente es un plan de pago desde US$ 5 al mes. La base de datos viene de una app de backend de Back4app, que también es gratuita para empezar: Parse Server gestionado con REST, SDKs, autenticación y Cloud Code. El container habla con ella por la red con dos variables de entorno.

¿Dónde pongo mis claves de API?

En las variables de entorno del container (Settings → Environment). Están disponibles en el build y en tiempo de ejecución, y un cambio solo se aplica en el siguiente deploy: la dashboard ofrece un botón Deploy now justo después de guardar. Nunca hornees claves dentro de la imagen.

¿Tengo que definir el schema de la base de datos primero?

No. El primer save desde el container creó la clase Link con sus columnas inferidas (Parse Server 7.5.2 en Back4app, septiembre de 2026). Recomendamos añadir el hook de validación antes del tráfico real, porque sin él el backend aceptó un Link cuya url era la cadena 'not a url'.

Compara con

Cómo crear y desplegar una API REST con Node.js en 15 minutos sin administrar un servidor →

Lecturas complementarias

Revisado y verificado por Back4app Engineering, Editorial review en Back4app

Probado el 10 de septiembre de 2026 — Containers: Back4app, plan Free, 0,25 vCPU, 256 MB · Backend: Back4app, USA East · Local: Node 25, Express 5.

Historial de cambios

  • 15 de septiembre de 2026 — Los containers gratuitos ahora reciben una URL temporal de 60 minutos (medido: 14:55:28 → 15:55:27). Corregidos el párrafo del plan gratuito, la FAQ y el contracaso; eliminados los nombres de hosts de terceros.

Más de Back4app Engineering

Backend · tutorial

Cómo crear y desplegar una API REST con Node.js en 15 minutos sin administrar un servidor

Containers · tutorial

Cómo desplegar FastAPI con Docker y una base de datos sin administrar un servidor

Containers · tutorial

Cómo desplegar una app Next.js con SSR usando Docker y un backend