Serviços de contêineres gerenciados¶
Nem toda equipe opera Kubernetes, e nem toda equipe quer manter uma máquina virtual. Azure Container Apps, Google Cloud Run e AWS ECS Fargate executam as mesmas imagens do Turbo EA sem um cluster para administrar, com o PostgreSQL gerenciado da mesma nuvem. Esta página oferece para cada um deles um modelo pronto para editar em deploy/ e diz com clareza o que cada plataforma consegue e não consegue fazer. Se você tem um cluster, a página Kubernetes e nuvem e o chart Helm são a melhor escolha; em um único host, o Docker Compose continua sendo o caminho mais simples. Tudo o que Operações e atualizações diz sobre backups, atualizações e a guarda de SECRET_KEY vale aqui sem alterações. Cada uma das três plataformas tem também um módulo Terraform que cria o mesmo grupo de contêineres junto com o banco de dados gerenciado — consulte Terraform.
O AWS App Runner fica de fora de propósito: deixou de aceitar novos clientes em abril de 2026 e nunca deu suporte a sidecars nem a volumes persistentes.
O formato comum¶
flowchart LR
U[Navegador / cliente MCP] --> LB[Ingress da plataforma ou balanceador<br/>TLS termina aqui]
subgraph G[Um grupo de contêineres — Container App / serviço Cloud Run / tarefa ECS]
N[nginx de borda :8920] --> F[frontend :8080]
N --> B[backend :8000 — uma instância]
N -. opcional .-> M[servidor MCP :8001]
M --> B
end
LB --> N
B --> P[(PostgreSQL gerenciado)]
B --> V[(/app/data persistente<br/>Azure Files · Filestore · EFS)]
Os três modelos constroem a mesma coisa:
- Um grupo de contêineres, sidecars em
localhost. O nginx de borda, o frontend, o backend e o servidor MCP opcional rodam como sidecars compartilhando um namespace de rede, então a borda encaminha parahttp://127.0.0.1:8000,:8080e:8001. Uma URL pública, um ciclo de vida, um deploy. - A borda escuta na 8920. Sua porta padrão é a 8080, que a imagem do frontend já ocupa no mesmo namespace; por isso cada modelo define
NGINX_HTTP_PORT=8920e aponta o ingress da plataforma para ela. A borda continua dona de todos os cabeçalhos de segurança, do limite de 512 MB para importações de espaço de trabalho, das configurações do fluxo de eventos e do roteamento/mcp— nada do lado da plataforma a substitui. - Um backend, nunca escalado, nunca a zero. O backend guarda estado interno ao processo (o barramento de eventos em tempo real, o limitador de requisições, o cache de permissões) e executa loops em segundo plano, portanto roda como exatamente uma instância com CPU alocada o tempo todo: número mínimo e máximo de instâncias igual a um em todas as plataformas.
- Os deploys se sobrepõem em duas das três plataformas. Container Apps e Cloud Run mantêm a instância antiga servindo até a nova ficar pronta, então por alguns segundos ou minutos dois backends rodam lado a lado em cada deploy. Por isso o backend toma um advisory lock do PostgreSQL em torno das migrações e da semeadura na inicialização: a segunda instância espera, encontra o esquema já atualizado e continua. Os loops em segundo plano ainda se duplicam nessa janela; eles são idempotentes. O ECS para a tarefa antiga antes de iniciar a nova (um ou dois minutos de indisponibilidade por deploy) e não precisa desse cuidado.
- Um
/app/datapersistente, de propriedade do uid 1000, guarda as extensões instaladas, os uploads e os pacotes de transferência de espaço de trabalho. Cartões e diagramas ficam no PostgreSQL. - O TLS termina na borda da plataforma.
TURBO_EA_TLS_ENABLEDpermanecefalse; umpublicUrlcomeçando comhttps://é o que marca o cookie de sessão comosecuree alimenta o CORS. - Os segredos vêm do cofre da plataforma — segredos do Container Apps ou Key Vault, Secret Manager, Secrets Manager — nunca como literais no modelo.
- As tags de imagem são o número da versão.
2.141.0executa as imagens2.141.0em todas as plataformas.
O que cada plataforma consegue e não consegue fazer¶
| Azure Container Apps | Google Cloud Run | AWS ECS Fargate | |
|---|---|---|---|
/app/data persistente |
Azure Files (SMB) montado com uid=1000 |
Somente Filestore via NFS — 100 GiB regional (duas regiões) ou 1 TiB nas demais; Cloud Storage FUSE não é POSIX, só para avaliação | EFS por meio de um ponto de acesso (uid/gid 1000) |
| Parar a instância antiga antes da nova | Não no modo de revisão única; sim com várias revisões e uma desativação manual | Não — as revisões sempre se sobrepõem | Sim (minimumHealthyPercent 0, maximumPercent 100) |
| Fluxo de eventos (SSE de longa duração) | Cortado a cada 240 s pelo ingress; o navegador reconecta | Até 3600 s por requisição, depois reconecta | Tempo ocioso do balanceador até 4000 s |
| Maior upload (a importação de espaço de trabalho chega a 512 MB) | Não documentado pela Microsoft — teste uma importação de 512 MB antes de confiar nisso | 32 MiB por requisição em HTTP/1 | Sem limite da plataforma |
| TLS e domínio próprio | Certificado gerenciado no app | Balanceador externo global + NEG serverless + certificado gerenciado pelo Google | Certificado ACM no ALB |
| Segredos | Segredos do app ou referências ao Key Vault | Secret Manager | Secrets Manager |
| Shell em um contêiner | az containerapp exec |
nenhum | ECS Exec |
| Sistema de arquivos raiz somente leitura | indisponível | não é uma configuração | possível, mas desativa o ECS Exec (desligado no modelo) |
Azure Container Apps¶
Pré-requisitos.
- Um grupo de recursos e um Azure Database for PostgreSQL Flexible Server alcançável a partir do ambiente: integrado à VNet (com sua própria sub-rede delegada na mesma VNet) ou acessível por um ponto de extremidade privado. O servidor exige TLS e o negocia automaticamente; o nome de usuário é o nome simples do papel.
- Para acesso privado ao banco, uma sub-rede de pelo menos
/27delegada aMicrosoft.App/environments, passada comoinfrastructureSubnetId. Deixe-a vazia apenas para uma avaliação contra um servidor acessível publicamente. - Uma conta de armazenamento com um compartilhamento de arquivos para
/app/data:az storage account create -g turbo-ea -n turboeadata -l westeurope --sku Standard_LRS az storage share-rm create --storage-account turboeadata --name turbo-ea-data --quota 50 - Dois segredos —
SECRET_KEY(openssl rand -base64 48) e a senha do banco — passados como parâmetros seguros ou referenciados do Key Vault (veja o comentário no modelo).
Implantar. Edite deploy/azure-container-apps/main.bicepparam, exporte os três segredos que ele lê do ambiente e execute:
export TURBO_EA_SECRET_KEY="$(openssl rand -base64 48)"
export TURBO_EA_POSTGRES_PASSWORD='…'
export TURBO_EA_STORAGE_KEY="$(az storage account keys list -n turboeadata --query '[0].value' -o tsv)"
az deployment group create -g turbo-ea \
-f deploy/azure-container-apps/main.bicep -p deploy/azure-container-apps/main.bicepparam
A implantação devolve o FQDN padrão do app. Na primeira execução, defina publicUrl como https://<esse FQDN>; quando vincular um domínio próprio com certificado gerenciado (az containerapp hostname add e depois az containerapp hostname bind --validation-method CNAME), defina publicUrl como esse domínio e implante de novo — a URL do navegador precisa coincidir com publicUrl para cookies e CORS. O primeiro usuário a se registrar torna-se administrador.
Atualizações. Altere imageTag e implante de novo. No modo de revisão única (padrão do modelo) a réplica antiga e a nova se sobrepõem por um instante; o lock de inicialização do backend torna isso seguro. Para uma semântica estrita de parar-e-depois-iniciar, mude o app para o modo de várias revisões, desative a revisão em execução e então implante:
az containerapp revision set-mode -g turbo-ea -n turbo-ea --mode Multiple
az containerapp revision deactivate -g turbo-ea -n turbo-ea --revision <atual>
az deployment group create … # depois ative a nova revisão e direcione o tráfego para ela
Limites a conhecer. O ingress encerra toda requisição após 240 segundos, então o fluxo de eventos reconecta a cada quatro minutos — o aplicativo tolera isso, mas aparece como um breve atraso após cada reconexão. A Microsoft não documenta um limite de tamanho de requisição; teste uma importação de espaço de trabalho de tamanho realista antes de confiar nisso. O Container Apps não tem sistema de arquivos raiz somente leitura nem configurações de contexto de segurança; as imagens já rodam com um usuário não root. As sondas limitam failureThreshold a 10, por isso a sonda de inicialização do backend consulta a cada 30 segundos para um orçamento de cinco minutos.
Google Cloud Run¶
Pré-requisitos.
- Uma VPC e uma sub-rede para a saída direta de VPC; por ela o serviço alcança o Cloud SQL e o Filestore.
- Uma instância do Cloud SQL para PostgreSQL com IP privado nessa VPC (acesso a serviços privados). O modelo conecta por
host:port, sem proxy. - Uma instância do Filestore para
/app/data— a única opção persistente e totalmente POSIX no Cloud Run. Seu compartilhamento pertence ao root na criação, então execute uma vez um job que o torne gravável pelo uid 1000 antes do primeiro deploy:gcloud filestore instances create turbo-ea-data --zone=europe-west1-b --tier=BASIC_HDD \ --file-share=name=share,capacity=1TB --network=name=default gcloud run jobs create chown-data --image=alpine --region=europe-west1 \ --network=default --subnet=default --add-volume=name=data,type=nfs,location=FILESTORE_IP:/share \ --add-volume-mount=volume=data,mount-path=/data --command=chown --args=-R,1000:1000,/data gcloud run jobs execute chown-data --region=europe-west1 --wait - Dois segredos do Secret Manager,
turbo-ea-secret-keyeturbo-ea-postgres-password, e uma conta de serviço de execução comroles/secretmanager.secretAccessoreroles/cloudsql.client. - O Cloud Run não consegue baixar diretamente do
ghcr.io. Crie uma vez um repositório remoto do Artifact Registry e referencie as imagens por ele, como faz o modelo:gcloud artifacts repositories create ghcr --repository-format=docker --location=europe-west1 \ --mode=remote-repository --remote-docker-repo=https://ghcr.io
Implantar. Substitua cada marcador UPPER_CASE em deploy/cloud-run/service.yaml — projeto, região, rede, IP do Filestore, IP privado do Cloud SQL, URL pública — e aplique:
gcloud run services replace deploy/cloud-run/service.yaml --region europe-west1
Para o nome de host público, coloque na frente do serviço um balanceador de carga de aplicativo externo global com um NEG serverless e um certificado gerenciado pelo Google (gcloud compute network-endpoint-groups create … --network-endpoint-type=serverless --cloud-run-service=turbo-ea), aponte o DNS para ele, defina TURBO_EA_PUBLIC_URL, ALLOWED_ORIGINS e MCP_PUBLIC_URL no manifesto para esse nome de host, aplique de novo e então mude a anotação de ingress para internal-and-cloud-load-balancing, para que a URL run.app pare de responder. Os mapeamentos de domínio no Cloud Run ainda estão em prévia e não são recomendados para produção.
Atualizações. Altere as tags de imagem e aplique o manifesto de novo. A nova revisão inicia enquanto a antiga ainda serve; o lock de inicialização do backend impede que as duas migrem ao mesmo tempo, e o Cloud Run não tem opção de parar a antiga primeiro.
Limites a conhecer. O Cloud Run rejeita corpos de requisição acima de 32 MiB em HTTP/1, então uma importação de transferência de espaço de trabalho maior falha no Cloud Run; execute importações grandes no Kubernetes ou em uma VM. O fluxo de eventos é encerrado após 3600 segundos e reconecta. O tamanho mínimo do Filestore é o custo dominante dessa configuração; um bucket do Cloud Storage montado via FUSE é barato mas não é POSIX (sem bloqueios, a última gravação vence), então serve para um teste e não para extensões instaladas em produção, e um volume em memória perde /app/data a cada revisão.
AWS ECS Fargate¶
Pré-requisitos.
- Uma VPC com duas sub-redes públicas (balanceador) e duas privadas (tarefa, destinos de montagem do EFS) com acesso NAT para baixar imagens do
ghcr.io. - Uma instância do RDS para PostgreSQL nas sub-redes privadas. Passe seu grupo de segurança como
DbSecurityGroupIde a pilha abre a porta 5432 a partir da tarefa; caso contrário, abra você mesmo usando a saídaTaskSecurityGroupId. - Um certificado do ACM para o nome de host público, na mesma Região.
- Dois segredos do Secrets Manager com
SECRET_KEYe a senha do banco como strings simples (para um segredo JSON gerenciado pelo RDS, acrescente:password::ao ARN no parâmetro).
Implantar.
aws cloudformation deploy --template-file deploy/ecs-fargate/template.yaml \
--stack-name turbo-ea --capabilities CAPABILITY_IAM \
--parameter-overrides VpcId=vpc-… PublicSubnetIds=subnet-a,subnet-b PrivateSubnetIds=subnet-c,subnet-d \
PublicUrl=https://ea.example.com ImageTag=2.141.0 DbHost=turbo-ea.abc.eu-central-1.rds.amazonaws.com \
DbPasswordSecretArn=arn:aws:secretsmanager:… SecretKeySecretArn=arn:aws:secretsmanager:… \
CertificateArn=arn:aws:acm:… DbSecurityGroupId=sg-…
Aponte o nome de host para a saída AlbDnsName (CNAME ou alias do Route 53) e abra-o. A pilha cria o cluster, um sistema de arquivos EFS criptografado com um ponto de acesso de propriedade do uid 1000, o balanceador com um listener HTTPS e um redirecionamento HTTP, e um serviço que executa uma tarefa.
Atualizações. Implante de novo com um novo ImageTag. O serviço para a tarefa em execução antes de iniciar a substituta — um parar-e-depois-iniciar de verdade, então o backend nunca é duplicado, ao custo de um ou dois minutos de indisponibilidade por deploy.
Operação. O EFS é protegido pelo AWS Backup (o modelo ativa a política padrão); combine seus pontos de restauração com os snapshots do RDS. aws ecs execute-command --cluster turbo-ea --task <id> --container backend --interactive --command sh abre um shell em qualquer contêiner. O tempo ocioso do balanceador é elevado a 4000 segundos para o fluxo de eventos, e ele não impõe limite de tamanho de corpo.
Solução de problemas¶
| Sintoma | Causa e correção |
|---|---|
| O nginx de borda nunca fica saudável, embora os logs do backend estejam bons | Em um layout com sidecars as variáveis de upstream precisam apontar para 127.0.0.1, e NGINX_HTTP_PORT precisa coincidir com a porta que o ingress da plataforma aponta (8920 em todos os modelos). |
| O backend registra another Turbo EA instance holds the startup lock — waiting | Esperado por um instante durante um deploy no Container Apps ou no Cloud Run. Se nunca desaparecer, a revisão antiga está travada: desative-a (Container Apps) ou exclua-a (Cloud Run). |
Permission denied em /app/data |
O volume não pertence ao uid 1000: confira as opções de montagem do Azure Files, execute o job chown do Filestore ou verifique o usuário POSIX do ponto de acesso do EFS. |
| O login entra em loop ou a API responde 401 no navegador | publicUrl (e os valores TURBO_EA_PUBLIC_URL / ALLOWED_ORIGINS derivados dele) não corresponde à URL na barra de endereços. |
| As atualizações em tempo real pausam a cada quatro minutos no Container Apps | O tempo limite de requisição do ingress; o navegador reconecta e nada se perde. |
| Uma importação de espaço de trabalho falha em 32 MiB no Cloud Run | O limite HTTP/1 da plataforma para corpos de requisição; execute importações grandes no Kubernetes ou em uma VM. |
O backend registra too many connections |
O plano gerenciado limita as conexões abaixo de DB_POOL_SIZE + DB_MAX_OVERFLOW; reduza o pool (orçamento de conexões). |