Como fazer o deploy de um app Node.js com Docker e banco de dados, sem administrar um servidor

Um encurtador de links com Dockerfile, publicado e no ar com banco de dados em 42 segundos, sem servidor para provisionar. Medido passo a passo, incluindo as quatro coisas que deram errado.

Um Dockerfile de 8 linhas vira um container em execução na Back4app, que guarda seus objetos Link em um backend Back4app — do push ao ar medido em 42 segundos

Tradução do original em inglês, testado em 10 de setembro de 2026. Código, números e datas são os do original. Read in English

Seu app roda em localhost. Ele tem um Dockerfile. E toda página de hospedagem que você abre responde à pergunta “onde meu código roda?” — mas não à que realmente tira seu sono: “onde meus dados ficam, e quem está aplicando patches naquele servidor?”

Este post trata do caso comum: um app web em Node.js, Express servindo uma página e duas rotas, que precisa de um lugar para rodar e de um lugar para guardar seus dados. Uma plataforma de containers roda seu código. Um backend dá o banco de dados, as regras e a autenticação que você não escreveu. A maioria dos apps precisa dos dois; poucas plataformas entregam os dois.

O mesmo formato tem outros três posts, um por variante: uma API Python com FastAPI, um app Next.js renderizado no servidor e um worker que nunca pode dormir.

Construímos um encurtador de links — 42 linhas de Express, um Dockerfile de 8 linhas —, fizemos o push e ele passou a guardar dados atrás de HTTPS sem nenhum servidor para administrar. O primeiro deploy levou 42 segundos do clique ao 200. Os redeploys levaram de 14 a 33 segundos. Tudo abaixo vem dessa execução, em 10 de setembro de 2026, incluindo as partes que quebraram.

Pegue o código funcionando: o repositório companion está em github.com/templates-back4app/link-shortener — server.js, Dockerfile, cloud/main.js e deploy-check.sh, que verifica um deployment de ponta a ponta. Clone, dê uma estrela ou abra uma issue quando algo sair do lugar.

O que está incluído em “sem administrar um servidor”?

Diga a frase em voz alta e ela soa como uma coisa só. São cinco. Uma plataforma só pode afirmar isso com honestidade se cobrir a lista inteira:

O que você precisaPor que não é opcionalOnde aparece neste post
Seu código rodandoA parte óbviaContainer, seção 03
HTTPS em uma URL públicaNavegadores e lojas de apps recusam HTTP puroContainer, automático
Um banco de dados que você não operaOs dados sobrevivem ao containerBackend, seção 04
Validação e auth fora do containerO container é descartável; as regras, nãoBackend, seção 05
Redeploy a cada mudança, sem downtimeSenão você volta ao SSHSeção 06

A maioria dos hosts de container para nas duas primeiras: rodam o Dockerfile e entregam uma URL. O banco de dados é um serviço à parte, cobrado à parte, e a auth fica por sua conta.

A Back4app é a que testamos porque entrega a segunda metade como produto: um app de backend com banco de dados gerenciado, auth, REST e Cloud Code, com o qual um app de container conversa pela rede. Dois apps, uma conta, e o resto deste post é o passo a passo medido pelos dois.

O container é a única peça para a qual você constrói uma imagem. Tudo à direita dele é gerenciado.

Um encurtador de links — o menor app que exercita as duas metades. O container serve uma página e duas rotas: POST /shorten cria um objeto Link no backend, GET /:code o busca e redireciona. O backend é dono dos dados e das regras.

Arquitetura: o navegador chama seu container em b4a.run; o container lê e grava objetos Link no backend Back4app pelo SDK; um hook beforeSave em Cloud Code valida cada Link antes de ele chegar ao banco de dados gerenciado

Quatro arquivos importam. O Dockerfile inteiro:

# 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"]

O container lê sua configuração do ambiente — nada mais vai embutido na imagem:

// 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 · encurtador de links · setembro de 2026
42linhas no container
→
19linhas de Cloud Code
0servidores para atualizar

Você precisa de uma conta Back4app gratuita, de uma conta no GitHub (o container faz o deploy a partir de um repositório) e de curl. Nada de Docker na sua máquina: este post inteiro foi produzido em um Mac sem Docker instalado, porque a plataforma constrói a imagem.

Como fazer o deploy de um Dockerfile Node.js a partir do GitHub?

Duas coisas precisam existir antes: o backend com o qual o container vai conversar e um repositório GitHub que a plataforma consiga ler. Fizemos o backend primeiro para que suas chaves já estivessem prontas.

Backend. Na dashboard, New App → Build your Backend, dê um nome, Create. Oitenta segundos depois a página Overview mostra o App ID e, no menu Keys, a JavaScript key — os dois valores de que o container precisa.

A página Overview do backend link-shortener: App ID e o menu Keys à esquerda, Parse Server 7.5.2 e MongoDB 3.6 à direita

Repositório. Faça o push dos quatro arquivos para o GitHub. Depois, do lado de Containers, New App → Deploy from GitHub → Import GitHub Repo. Isso abre a página de instalação do app Back4App Containers no GitHub: escolha Only select repositories, selecione o repo, Install & Authorize. Dezesseis segundos depois desse clique o repo estava listado na dashboard.

A tela Deploy a web app listando o repositório link-shortener com um botão Select, depois de o GitHub App ter sido instalado

Configurar e fazer o deploy. Selecione o repo e o formulário pede um nome, o branch e um método de build — ele detectou o Dockerfile. Deixamos a porta vazia (a plataforma lê o EXPOSE), apontamos o health check para /healthz e adicionamos duas variáveis de ambiente: PARSE_APP_ID e PARSE_JS_KEY. Depois, Deploy.

Clicamos às 11:12:45. O log seguiu PREPARING → FETCHING GITHUB REPOSITORY → BUILDING IMAGE → Pushed image → LAUNCHING CONTAINER → CHECKING HEALTH → DEPLOYMENT READY, e /healthz respondeu 200 às 11:13:27.

O Overview do container depois do primeiro deploy: status Available, build Ready em 29 s, o log de deployment terminando em DEPLOYMENT READY e o painel de configuração mostrando Dockerfile, porta auto, 2 variáveis e health check /healthz

Medido · primeiro deploy · 10 de setembro de 2026
42 sclique em Deploy → primeiro 200
→
29 sdesse total, build da imagem
64,6 MBimagem comprimida · 8 camadas

Como medimos: um cronômetro do clique em Deploy até o primeiro 200 da URL pública, no plano Free (0,25 vCPU, 256 MB), um deploy em 10 de setembro de 2026. Os tempos de redeploy são quatro pushes separados no mesmo dia. Seu tempo de build varia com o tamanho da imagem e a velocidade do registry; os números aqui são referência, não promessa.

A URL é https://linkshortener-<id>.b4a.run — HTTPS incluído, sem certificado para pedir. O container gratuito recebe 0,25 vCPU, 256 MB e 100 GB de transferência.

A página do encurtador de links no ar, servida de linkshortener-awjh19o8.b4a.run: um campo de URL e um botão Shorten

Como guardar dados sem um banco de dados?

Você já fez isso, no código acima: link.save() envia o objeto ao backend por HTTPS, usando o App ID e a chave vindos do ambiente. Não há connection string, nem pool, nem processo de banco de dados dentro do container.

Aqui está a mesma gravação de dois jeitos. O container usa o SDK; a forma REST é o que o SDK envia, e funciona de qualquer lugar — inclusive do seu terminal, com suas próprias chaves:

// 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"

Mesma gravação, mesmo backend. A forma REST é o que o SDK envia por baixo dos panos.

A primeira gravação a partir do container criou a classe Link sozinha — nenhum schema foi definido antes (Parse Server 7.5.2 na Back4app, setembro de 2026). Segundos depois os objetos estavam visíveis no Database Browser da dashboard, com as colunas url, code e clicks inferidas do que o container gravou.

O Database Browser mostrando a classe Link com três objetos e suas colunas url, clicks e code

Latência, medida de um laptop em UTC−3 com 10 amostras cada: uma gravação que passa pelo container até o backend levou uma mediana de 205 ms de ponta a ponta; a mesma gravação enviada diretamente ao backend levou 440 ms. O container e o backend ficam na mesma região (USA East), então o salto entre eles é a parte barata da viagem.

Onde vivem a validação e a geração do código curto?

Tudo o que precisa sobreviver à substituição do container: as regras sobre seus dados, as chaves e os usuários. Na nossa primeira execução o backend aceitou um Link cuja url era a string not a url, e não devolveu código curto nenhum — porque nada tinha dito a ele o que é um Link válido. Esse é o momento de adicionar o hook.

cloud/main.js roda dentro do backend, não no container. Ele rejeita URLs inválidas, define clicks como 0 e aloca um código que nenhum outro Link usa:

// 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.");
});

Cole em Cloud Code → cloud/main.js na dashboard do backend e Deploy. Nosso hook estava respondendo 21 segundos depois do clique. A partir daí o POST /shorten do container passou a devolver um code, e not a url voltou como 400 {"code":142,"error":"url must be an http(s) URL."} — o código do container não mudou em nada.

O editor de Cloud Code na dashboard do backend com cloud/main.js aberto, mostrando o hook beforeSave

A mesma regra vale para chaves e usuários. O container guarda a chave JavaScript, que é uma chave de cliente — ela faz exatamente o que um navegador faria. A master key (chave mestra) nunca sai do backend, e a classe User do Parse, as sessões e as ACLs estão lá para quando o encurtador ganhar donos.

O que acontece quando você faz um redeploy?

O auto deploy fica desligado em containers gratuitos — o toggle aparece desabilitado —, então um git push sozinho não muda nada na URL pública. Fizemos o push de uma mudança de uma linha às 11:26 e o site continuou servindo a versão antiga até fazermos o deploy à mão.

Deploy latest commit não faz o deploy na hora. Primeiro abre uma revisão Finish your setup: a plataforma tinha lido o repo como “Docker / Node.js / Express · Node.js 22 Alpine” e sugeriu node server.js e a porta 8080. Nada é obrigatório; Deploy without changes inicia o build.

O diálogo Finish your setup: a plataforma leu o repo como Docker / Node.js / Express em Node.js 22 Alpine, sugere o comando de início node server.js e a porta 8080, com os botões Deploy without changes e Save and deploy

Medimos quatro redeploys. Um build com cache e uma mudança de uma linha foi do clique à nova versão respondendo em 14 s; os outros levaram 20, 32 e 20 s. Durante cada um deles, uma requisição a cada cinco segundos nunca viu nada além de 200: a lista de deployments mostra o container antigo como Destroying só depois que o novo está Ready.

A lista de Deployments depois de vários redeploys: o deployment mais novo Ready em 20 s, o anterior Destroying, os mais antigos Destroyed

Variáveis de ambiente recebem o mesmo tratamento: mude uma em Settings → Environment e a dashboard pergunta “Settings saved — deploy now with the settings you just saved?”. Uma rotação de chave é um redeploy, não uma sessão de SSH.

A página de configurações de variáveis de ambiente listando PARSE_APP_ID e PARSE_JS_KEY, com a nota de que uma mudança entra em vigor no próximo deploy

Quais erros o deploy do Node.js apresentou, e o que corrigiu cada um?

Quebramos o deployment de propósito três vezes. Dois dos três casos não se comportaram como os guias de erro costumam prometer.

App is not listening on port 8080, but on port 3000 — switching to it (auto-detected). Definimos PORT=3000 para que o app ignorasse a linha EXPOSE 8080. Esperado: um health check falho. Real: a plataforma sondou, encontrou a 3000, registrou a linha acima, salvou 3000 nas configurações do app e reportou DEPLOYMENT READY em 20 s. Remover a variável trouxe tudo de volta do mesmo jeito. Uma porta diferente não é falha aqui.

O log de deployment com as linhas App is not listening on port 8080, but on port 3000 — switching to it (auto-detected) e Port 3000 saved to the app settings for future deploys, seguidas de DEPLOYMENT READY

{"error":"unauthorized"} com HTTP 502 em /shorten, mas o deploy disse Ready. Fizemos o deploy com uma PARSE_JS_KEY errada. O health check bate em /healthz, que nunca toca o backend, então o container quebrado passou e foi ao ar; a falha só apareceu na primeira requisição real. Corrija a chave em Settings → Environment e faça o deploy. Melhor ainda: faça seu health check exercitar o backend.

Links curtos em http://… embora o site seja HTTPS. A plataforma termina o TLS e — em setembro de 2026 — não repassa o scheme, então o req.protocol do Express é http mesmo com trust proxy ligado. Monte as URLs públicas com https:// explicitamente, como o server.js faz agora.

Runtime Logs não mostram nada depois de uma falha. Os Runtime Logs mostram o stdout e o stderr do seu container, marcados com o id do container — linhas de console.log como SYSTEM, console.error como ERROR. Nossa primeira versão engoliu o erro unauthorized e não deixou rastro. Registre suas falhas; aí a página de logs é o primeiro lugar para olhar.

Runtime Logs mostrando link-shortener listening on 8080 e uma linha ERROR shorten failed: 142 url must be an http(s) URL., cada uma marcada com o id do container

Website blocked — known phishing site ao abrir sua URL b4a.run. Nossa URL recém-criada foi bloqueada pelo Threat Protection da NordVPN no Chrome, enquanto o curl a alcançava normalmente. O domínio compartilhado carrega a reputação de todos os apps que estão nele. Verifique primeiro com curl -I e coloque um domínio próprio em qualquer coisa que você mostre a usuários.

Nothing here, yet! depois de instalar o GitHub App. O callback de autorização caiu em um navegador que não estava logado na dashboard. Desinstale o app no GitHub (Settings → Applications) e refaça Import GitHub Repo a partir da dashboard, no mesmo navegador.

Success on deploying your changes! mas o Cloud Code não faz nada. Confira Logs → System procurando main.js not found; faça a edição no próprio editor para que “Files pending deploy” apareça, e então faça o deploy de novo.

Quando um container é a resposta errada?

Pelo nosso próprio manifesto, comparações em que só a plataforma ganha não são publicadas — então aqui está a outra coluna.

  • Seu app é CRUD e nada mais. Então você não precisa de container nenhum: a camada REST do backend já é a API. Nosso post sobre API REST constrói uma em 24 linhas e zero containers.
  • Você precisa de um motor ou de uma extensão de banco de dados específicos. O backend é Parse Server sobre MongoDB (Postgres nos planos pagos). Se seu app é PostGIS ou ClickHouse, a metade de dados deste post não se aplica.
  • Você precisa que a URL dure mais de uma hora, de auto deploy ou de mais de 0,25 vCPU. Os três são recursos pagos aqui desde 15 de setembro de 2026 (os planos Shared começam em US$ 5/mês); consulte a página de preços, e não este post, para os números atuais.
  • Você quer construir imagens localmente e enviá-las. A rota por imagem aceita apenas o Docker Hub — nossa imagem no GitHub Container Registry foi recusada no formulário.

Se nenhum desses casos descreve você, o formato do diagrama — um container descartável na frente de um backend gerenciado — é o que escolheríamos de novo.

Você tem um container que faz o redeploy em menos de meio minuto, um banco de dados que você nunca provisionou e uma validação que sobrevive à substituição do container. A partir daqui:

  • Dê donos aos links. A classe User do Parse adiciona endpoints de criar conta e entrar sem nenhum código no container (medido, com redefinição de senha); um beforeSave pode gravar o usuário criador em cada Link e uma ACL que só ele pode editar (como as travas se comportam).
  • Coloque um domínio. Settings → Domains no container — a solução para o problema de reputação acima.
  • Torne o health check honesto. Faça o /healthz rodar uma consulta barata no backend, para que uma chave errada derrube o deploy em vez do primeiro usuário.

Todo o código acima está completo e testado — o repositório companion inclui o deploy-check.sh, que roda health → shorten → redirect contra qualquer URL e sai com código diferente de zero se algo estiver errado.

Tagsdockercontainersnodeexpressparse-server

Perguntas frequentes

Dá para fazer o deploy de um container Docker sem administrar um servidor?

Sim. No Back4app Containers você conecta um repositório GitHub com um Dockerfile; a plataforma constrói a imagem (kaniko), roda o container, termina o HTTPS e entrega uma URL. Na nossa execução medida, o primeiro deploy foi do clique em Deploy a uma resposta 200 em 42 segundos (setembro de 2026).

O plano gratuito inclui banco de dados?

O plano de container em si, não — ele dá 0,25 vCPU, 256 MB e 100 GB de transferência e, desde 15 de setembro de 2026, uma URL que dura 60 minutos por deploy; uma URL permanente é plano pago a partir de US$ 5/mês. O banco de dados vem de um app de backend da Back4app, que também é gratuito para começar: Parse Server gerenciado com REST, SDKs, auth e Cloud Code. O container fala com ele pela rede usando duas variáveis de ambiente.

Onde coloco minhas chaves de API?

Nas variáveis de ambiente do container (Settings → Environment). Elas ficam disponíveis no build e em tempo de execução, e uma mudança só entra em vigor no próximo deploy — a dashboard oferece um botão Deploy now logo depois de salvar. Nunca embuta chaves na imagem.

Preciso definir o schema do banco de dados antes?

Não. A primeira gravação feita pelo container criou a classe Link com as colunas inferidas (Parse Server 7.5.2 na Back4app, setembro de 2026). Recomendamos adicionar o hook de validação antes de receber tráfego real, porque sem ele o backend aceitou um Link cuja url era a string 'not a url'.

Compare com

Build and Deploy a Node.js REST API in 15 Minutes — Without Owning a Server [Full Code Inside] →

Leitura complementar

Revisado e verificado por Back4app Engineering, Editorial review na Back4app

Testado em 10 de setembro de 2026 — Containers: Back4app, plano Free, 0,25 vCPU, 256 MB · Backend: Back4app, USA East · Local: Node 25, Express 5.

Histórico de alterações

  • 15 de setembro de 2026 — Free containers now get a 60-minute temporary URL (measured: 14:55:28 → 15:55:27). Corrected the free-tier paragraph, FAQ and counter-case; removed named third-party hosts.

Mais da Back4app Engineering

Backend · tutorial

Build and Deploy a Node.js REST API in 15 Minutes — Without Owning a Server [Full Code Inside]

Containers · tutorial

Como fazer o deploy de FastAPI com Docker e banco de dados, sem administrar um servidor

Containers · tutorial

How to Deploy a Next.js SSR App With Docker and a Backend [Full Repo]