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ê 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.
O container é a única peça para a qual você constrói uma imagem. Tudo à direita dele é gerenciado.
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.
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") });
});
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.

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.

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.

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.

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"# Stack: bash | Endpoint: POST /classes/Link
curl -X POST \
-H "X-Parse-Application-Id: $APP_ID" \
-H "X-Parse-REST-API-Key: $REST_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.back4app.com/docs/"}' \
"$BASE/classes/Link"
# {"objectId":"…","createdAt":"…","code":"zASPnB"} ← HTTP 201Mesma 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.

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.

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.

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.

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.

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.

{"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.

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.
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
Userdo Parse adiciona endpoints de criar conta e entrar sem nenhum código no container (medido, com redefinição de senha); umbeforeSavepode 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
/healthzrodar 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.