flutter build web entrega uma pasta. Essa é toda a saída: um index.html, uma pilha de JavaScript com hash, algumas fontes e ícones. Nada ali diz onde isso deve morar.
Essa última parte é o trabalho. A pasta precisa de um servidor web que entenda roteamento no cliente, de uma máquina para rodá-lo, de uma URL e de um jeito de levar um build novo para lá quando você muda algo — e nada disso é tarefa do Flutter.
Este guia faz tudo isso de uma vez: um Dockerfile de dois estágios que compila o build e o serve com nginx, um repositório GitHub e um deploy no plano Free. Tudo abaixo foi medido em 23 de setembro de 2026.
Pegue o código funcionando: o repositório companion está em github.com/templates-back4app/flutter-web-on-back4app — o app, o Dockerfile, a configuração do nginx e dois testes de widget que passam. Faça um fork e você consegue fazer o deploy sem escrever nada.
Prefere assistir ao método antigo?
O tutorial em vídeo da própria Back4app, de 2021, mostra um caminho diferente: ativar um subdomínio de hospedagem na dashboard e então enviar o build com a ferramenta de linha de comando b4a.
O caminho antigo de hospedagem mudou?
Sim, de duas maneiras que importam antes de você começar. O caminho no menu mudou de lugar, e o recurso saiu do plano Free.
No vídeo, a hospedagem web fica em App Settings → Server Settings → Web Hosting and Custom Domains. Hoje é um link Configure na Overview do app que abre Domain Settings, e num app Free essa página traz uma única mensagem: “Please upgrade your plan to activate your web hosting.”

O caminho gratuito hoje é a entrada Web Deployment na barra lateral do app, que abre o Back4app Containers e pede um repositório GitHub ou uma imagem do Docker Hub. É o caminho que este guia segue.
O tutorial de 2021 envia uma pasta. O de 2026 entrega um repositório e deixa a plataforma construir a imagem. O que você escreve muda na mesma medida: um Dockerfile, não um passo de cópia.
O que vai no Dockerfile?
Dois estágios. O primeiro compila o build do Flutter; o segundo o serve. Mantê-los separados é o que impede um SDK de 4 GB de pegar carona na imagem que roda o seu site.
# Stack: Flutter 3.47.5 (web) + nginx 1.27 | File: Dockerfile
FROM debian:bookworm-slim AS build
ARG FLUTTER_VERSION=3.47.5
ENV PATH="/opt/flutter/bin:${PATH}"
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates git unzip xz-utils \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz" \
-o /tmp/flutter.tar.xz \
&& tar xf /tmp/flutter.tar.xz -C /opt \
&& git config --global --add safe.directory /opt/flutter
WORKDIR /app
COPY pubspec.yaml pubspec.lock ./
RUN flutter pub get
COPY . .
RUN flutter build web --release --dart-define=BUILD_STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
FROM nginx:1.27-alpine
COPY --from=build /app/build/web /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
O SDK é fixado numa versão exata e baixado do próprio armazenamento de releases do Google, em vez de uma imagem da comunidade. Imagens Flutter pré-construídas existem, mas a que a maioria dos guias usa estava quatro versões minor atrás da stable no dia em que testamos, o que silenciosamente significa que seu container compila com um Flutter diferente do seu laptop.
Copiar o pubspec.yaml antes do resto do código-fonte é a única otimização que vale a pena manter: a resolução de dependências vira uma camada em cache, e editar lib/ não a repete.
O que o nginx precisa saber?
Que suas rotas não são arquivos. O Flutter resolve caminhos no navegador, então uma requisição para /settings não tem nada por trás no disco, e um nginx com configuração padrão devolve 404 para todo deep link e todo refresh.
# Stack: nginx 1.27 | File: nginx.conf
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location = /index.html {
add_header Cache-Control "no-store, must-revalidate";
}
location ~* \.(js|wasm|json|png|svg|ttf|otf|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
As duas regras de cache formam um par. A saída do Flutter carrega hashes de conteúdo nos nomes dos arquivos, então esses arquivos podem ser cacheados com força e por muito tempo; o index.html é o único arquivo cujo nome nunca muda, e cacheá-lo é a receita para um deploy sair e ninguém ver.
Como fazer o deploy?
Faça o push do projeto para o GitHub e aponte o Back4app Containers para o repositório. Na barra lateral do app, Web Deployment → Deploy a Web App → GitHub repository.
Escolha o repositório, deixe o branch e o diretório raiz como estão, e a etapa de configuração se preenche sozinha. A nossa detectou o Dockerfile e definiu o runtime a partir dele, com a porta em auto: nada para digitar.

Depois, espere. O primeiro build não tem camadas em cache, então ele baixa o SDK e compila do zero — o nosso levou 9 minutos e 13 segundos e terminou em Ready, com o app marcado como Available numa URL b4a.run.

Funcionou de verdade?
Confira os headers, não só a página. Um app Flutter renderiza num canvas, então “parece certo” diz menos do que o normal, e as regras do nginx são a parte com mais chance de estar errada.
| Verificação | Resultado |
|---|---|
GET / | 200, server: nginx/1.27.5 |
GET /some-route-that-does-not-exist | 200 — o fallback da SPA funciona |
index.html | cache-control: no-store, must-revalidate |
flutter_bootstrap.js | cache-control: max-age=2592000, public, immutable |
Como medimos: uma rodada em 23 de setembro de 2026, curl -sI contra a URL do deploy logo depois que o deploy reportou Ready; a duração do build é o número que a página Deployments mostra para aquele deployment.

O que o plano Free não faz?
Três coisas que vale saber antes de planejar em cima dele, todas declaradas na dashboard, não inferidas.
A URL dura uma hora. A barra lateral diz “URL is temporary and will be live for 60 minutes”, ao lado de um botão de upgrade. É o bastante para conferir o seu trabalho, e não o bastante para mandar a um cliente.
Autodeploy no push é pago. Build and deploy diz isso onde a opção estaria, então no plano Free todo build novo é uma ação manual na dashboard.
Não existe campo para argumentos de build do Docker. Build and deploy oferece o método de build, o branch, o diretório raiz e a porta — nada mais. Variáveis de ambiente existem, mas são valores de runtime entregues ao container em execução, não ao build.
Quando um container é o lugar errado para um build Flutter?
Quando a saída é genuinamente estática e nada mais. Uma pasta de arquivos não precisa de um runtime de container, e um host estático comum ou um bucket com CDN vai servi-la com menos coisas para dar errado e sem cold start.
Quando você precisa que a URL seja permanente e gratuita. Este caminho dá uma URL permanente num plano pago; o plano Free serve para conferir que o build funciona.
E quando o seu app precisa de um backend de qualquer forma. Se o app Flutter fala com um banco de dados, as decisões interessantes estão desse lado, e onde os arquivos estáticos ficam é a metade menor do problema.
O que fazer em seguida?
Faça um fork do repositório e o deploy uma vez, antes de apontar o fluxo para o seu próprio app. É uma passada pela dashboard sem digitar nada, e ela diz se o GitHub App, o plano e o build se comportam na sua conta.
Depois troque pelo seu próprio lib/. O Dockerfile e a configuração do nginx não mudam — essa é a razão de colocá-los no repositório e não num campo da dashboard.
O hábito que vale manter é a conferência dos headers. Um deploy de Flutter web falha em silêncio: a página carrega, as rotas só quebram no refresh, e um index.html em cache esconde seu build mais novo pelo tempo que o navegador quiser. Quatro chamadas curl -sI pegam os três.
