# 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.

URL: https://www.back4app.com/blog/pt/como-fazer-o-deploy-de-um-app-nodejs-com-docker-e-banco-de-dados
Publicado: 2026-10-12 · Testado: 2026-09-10
Testado em: Containers: Back4app, plano Free, 0,25 vCPU, 256 MB · Backend: Back4app, USA East · Local: Node 25, Express 5
Publisher: Back4app Engineering

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](https://www.back4app.com/blog/deploy-python-fastapi-dockerfile-database), um [app Next.js renderizado no servidor](https://www.back4app.com/blog/deploy-nextjs-app-github-backend) e um [worker que nunca pode dormir](https://www.back4app.com/blog/run-a-bot-webhook-or-cron-24-7-without-a-vps).

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](https://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ê precisa | Por que não é opcional | Onde aparece neste post |
|---|---|---|
| Seu código rodando | A parte óbvia | Container, seção 03 |
| HTTPS em uma URL pública | Navegadores e lojas de apps recusam HTTP puro | Container, automático |
| Um banco de dados que você não opera | Os dados sobrevivem ao container | Backend, seção 04 |
| Validação e auth fora do container | O container é descartável; as regras, não | Backend, seção 05 |
| Redeploy a cada mudança, sem downtime | Senão você volta ao SSH | Seçã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.

## Como é o encurtador de links, arquivo por arquivo?

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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/architecture-diagram.svg)

Quatro arquivos importam. O Dockerfile inteiro:

```dockerfile
# 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:

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

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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/03-backend-keys-v2.jpg)

**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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/02-repo-picker.jpg)

**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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/04-container-ready.jpg)

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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/06-live-app.jpg)

## 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:

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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/05-database-link.jpg)

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:

```javascript
// 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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/07-cloud-code.jpg)

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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/08-finish-setup.jpg)

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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/09-deployments.jpg)

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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/10-env-settings.jpg)

## 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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/11-port-autodetect.jpg)

**`{"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](/blog/blog-assets/deploy-node-app-dockerfile-back4app/12-runtime-logs.jpg)

**`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](https://www.back4app.com/blog/build-rest-api-nodejs-back4app) 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.

## Para onde levar o encurtador de links a partir daqui?

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](https://www.back4app.com/blog/add-signup-login-password-reset-without-an-auth-server)); um `beforeSave` pode gravar o usuário criador em cada Link e uma ACL que só ele pode editar ([como as travas se comportam](https://www.back4app.com/blog/lock-down-a-backend-your-frontend-talks-to)).
- **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](https://github.com/templates-back4app/link-shortener) 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.
