O Studio guarda todo arquivo enviado — documentos de base de conhecimento, anexos de chat, saídas de execução, fotos de perfil e mais — em object storage. Há suporte para quatro backends:
| Backend | Quando usar |
|---|---|
| Disco local | Docker em nó único, desenvolvimento local, avaliação |
| AWS S3 | Produção, especialmente com mais de uma réplica do app |
| Azure Blob | Produção no Azure |
| Google Cloud Storage | Produção no GCP |
O disco local grava no diretório /uploads do contêiner. Os arquivos são perdidos quando o contêiner é recriado, a menos que esse caminho esteja em um volume persistente, e eles não são compartilhados entre réplicas. Para qualquer deployment com múltiplas réplicas ou em produção, use S3, Azure Blob ou Google Cloud Storage.
Como o backend é escolhido
Defina STORAGE_PROVIDER como local, s3, azure ou gcs para escolher um backend explicitamente. Quando ela não tem valor, o Studio infere o backend a partir das variáveis de ambiente configuradas, nesta ordem:
- Azure Blob — usado se
AZURE_STORAGE_CONTAINER_NAMEestiver definida e também (AZURE_ACCOUNT_NAME+AZURE_ACCOUNT_KEY) ouAZURE_CONNECTION_STRING. - AWS S3 — usado se
S3_BUCKET_NAMEeAWS_REGIONestiverem definidas (e o Azure não estiver configurado). - Google Cloud Storage — usado se
GCS_BUCKET_NAMEestiver definida (e nem o Azure nem o S3 estiverem configurados). - Disco local — o fallback quando nenhum está configurado.
Se STORAGE_PROVIDER não tiver valor, o Studio ignora backends incompletos e usa a primeira opção pronta nessa ordem. Um backend de prioridade mais alta com os campos obrigatórios presentes, mas com valores inválidos, falha imediatamente em vez de cair silenciosamente para o próximo. Um STORAGE_PROVIDER explícito tem precedência e precisa ser válido e completo.
Configurar o AWS S3
Criar os buckets
O Studio separa os arquivos em buckets por finalidade. O Studio nunca cria buckets — crie cada um antes de configurá-lo. Só S3_OG_IMAGES_BUCKET_NAME e S3_WORKSPACE_LOGOS_BUCKET_NAME caem para o bucket geral; os outros resolvem para os próprios nomes padrão literais, então defina todos os buckets que você pretende usar.
# Set your region once
export AWS_REGION=us-east-1
# Create buckets (names must be globally unique — prefix with your org)
for name in workspace-files knowledge-base execution-files chat-files \
copilot-files profile-pictures og-images workspace-logos; do
aws s3api create-bucket \
--bucket "myorg-studio-$name" \
--region "$AWS_REGION" \
--create-bucket-configuration LocationConstraint="$AWS_REGION"
doneEm us-east-1, omita a flag --create-bucket-configuration — essa região rejeita um LocationConstraint explícito.
Mantenha todos os buckets privados (bloqueie o acesso público). O Studio entrega os arquivos por URLs pré-assinadas de curta duração, então os buckets nunca precisam de leitura pública.
Configurar o CORS em cada bucket
Os uploads são enviados direto do navegador para o S3 via requisições PUT pré-assinadas, então cada bucket precisa de uma política de CORS que permita a origem do seu Studio. Sem isso, todo upload falha com erro de CORS no console do navegador, mesmo que a configuração no servidor esteja correta.
cat > /tmp/cors.json <<'EOF'
{
"CORSRules": [
{
"AllowedOrigins": ["https://studio.yourdomain.com"],
"AllowedMethods": ["GET", "PUT"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}
]
}
EOF
for name in workspace-files knowledge-base execution-files chat-files \
copilot-files profile-pictures og-images workspace-logos; do
aws s3api put-bucket-cors --bucket "myorg-studio-$name" --cors-configuration file:///tmp/cors.json
doneDefina AllowedOrigins com a origem exata do seu Studio (esquema + host, sem barra no final). Inclua todas as origens pelas quais as pessoas acessam o Studio, incluindo o par apex/www se os dois estiverem no ar.
Conceder acesso com uma política IAM
Crie uma política IAM restrita aos seus buckets e associe-a ao usuário (ou role) sob o qual o Studio é executado:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": [
"arn:aws:s3:::myorg-studio-*",
"arn:aws:s3:::myorg-studio-*/*"
]
}
]
}Você tem então duas formas de fornecer as credenciais:
- Chaves estáticas — crie um usuário IAM com essa política e defina
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY. - Credenciais de instância/role (recomendado) — associe a política à role da instância EC2, à task role do ECS ou à role IRSA do EKS. Deixe
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYsem valor e o Studio recorre automaticamente à cadeia de credenciais padrão da AWS.
Configurar as variáveis de ambiente
Defina a região, opcionalmente as credenciais, e os nomes dos buckets:
# Region + credentials
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=AKIA... # omit when using an instance/IRSA role
AWS_SECRET_ACCESS_KEY=... # omit when using an instance/IRSA role
# Buckets (per purpose)
S3_BUCKET_NAME=myorg-studio-workspace-files
S3_KB_BUCKET_NAME=myorg-studio-knowledge-base
S3_EXECUTION_FILES_BUCKET_NAME=myorg-studio-execution-files
S3_CHAT_BUCKET_NAME=myorg-studio-chat-files
S3_COPILOT_BUCKET_NAME=myorg-studio-copilot-files
S3_PROFILE_PICTURES_BUCKET_NAME=myorg-studio-profile-pictures
S3_OG_IMAGES_BUCKET_NAME=myorg-studio-og-images
S3_WORKSPACE_LOGOS_BUCKET_NAME=myorg-studio-workspace-logosSó AWS_REGION e S3_BUCKET_NAME são estritamente obrigatórias para colocar o Studio em modo S3. Adicione as outras para que cada tipo de arquivo vá para o próprio bucket.
Referência dos buckets S3
| Variável | Armazena | Obrigatória |
|---|---|---|
AWS_REGION | Região de todos os buckets | Sim (ativa o S3) |
AWS_ACCESS_KEY_ID | Access key | Não (usa a cadeia de credenciais se não definida) |
AWS_SECRET_ACCESS_KEY | Secret key | Não (usa a cadeia de credenciais se não definida) |
S3_BUCKET_NAME | Arquivos gerais do workspace | Sim (ativa o S3) |
S3_KB_BUCKET_NAME | Documentos da base de conhecimento | Recomendada |
S3_EXECUTION_FILES_BUCKET_NAME | Arquivos de execução de workflow. Cai para o nome literal studio-execution-files, que você quase certamente não possui — sempre defina esta explicitamente | Sim |
S3_CHAT_BUCKET_NAME | Ativos do chat com deploy | Recomendada |
S3_COPILOT_BUCKET_NAME | Anexos do Chat | Recomendada |
S3_PROFILE_PICTURES_BUCKET_NAME | Avatares de pessoas | Recomendada |
S3_OG_IMAGES_BUCKET_NAME | Imagens de preview OpenGraph (cai para S3_BUCKET_NAME) | Opcional |
S3_WORKSPACE_LOGOS_BUCKET_NAME | Logos de workspace (cai para S3_BUCKET_NAME) | Opcional |
S3_ENDPOINT | Endpoint customizado para armazenamento compatível com S3 (R2, MinIO, B2) | Opcional (AWS S3 se não definida) |
S3_FORCE_PATH_STYLE | true para endereçamento path-style (MinIO/Ceph) | Opcional (padrão false) |
Aplicar a configuração
Adicione as variáveis de armazenamento ao arquivo .env usado pelo docker-compose.prod.yml e reinicie:
docker compose -f docker-compose.prod.yml up -dComo os arquivos agora ficam no S3, você não depende mais de um volume local /uploads para durabilidade.
Defina as variáveis em app.env (as não sensíveis, por exemplo região e nomes de bucket) e forneça as credenciais via secret. O chart traz um exemplo completo em helm/studio/examples/values-aws.yaml:
app:
env:
AWS_REGION: "us-east-1"
S3_BUCKET_NAME: "myorg-studio-workspace-files"
S3_KB_BUCKET_NAME: "myorg-studio-knowledge-base"
S3_EXECUTION_FILES_BUCKET_NAME: "myorg-studio-execution-files"
# ...remaining bucketsNo EKS, prefira IRSA: associe a política IAM à role da service account e deixe as variáveis de access key sem valor.
Configurar o Azure Blob
O Azure Blob usa um contêiner por finalidade, espelhando o layout do S3. Autentique com uma connection string ou com nome da conta + chave.
# Credentials — provide ONE of these forms
AZURE_ACCOUNT_NAME=mystorageaccount
AZURE_ACCOUNT_KEY=...
# or
AZURE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net
# Containers (per purpose)
AZURE_STORAGE_CONTAINER_NAME=workspace-files
AZURE_STORAGE_KB_CONTAINER_NAME=knowledge-base
AZURE_STORAGE_EXECUTION_FILES_CONTAINER_NAME=execution-files
AZURE_STORAGE_CHAT_CONTAINER_NAME=chat-files
AZURE_STORAGE_COPILOT_CONTAINER_NAME=copilot-files
AZURE_STORAGE_PROFILE_PICTURES_CONTAINER_NAME=profile-pictures
AZURE_STORAGE_OG_IMAGES_CONTAINER_NAME=og-images
AZURE_STORAGE_WORKSPACE_LOGOS_CONTAINER_NAME=workspace-logosUploads diretos do navegador exigem uma regra de CORS do serviço Blob na storage account. Permita a
origem exata do seu Studio, os métodos GET e PUT, o header Content-Type e o prefixo x-ms-* usado
pelos headers assinados de blob e de metadados. Uploads de arquivos pequenos também enviam If-None-Match;
a própria assinatura é somente-criação, então uma URL assinada não pode sobrescrever um objeto final já existente.
Uploads multipart também leem o ETag da resposta no navegador:
az storage cors add \
--services b \
--methods GET PUT OPTIONS \
--origins https://studio.yourdomain.com \
--allowed-headers content-type if-none-match 'x-ms-*' \
--exposed-headers ETag \
--max-age 3600 \
--account-name mystorageaccount \
--account-key '<account-key>'Se você autenticar com uma connection string, substitua as duas últimas opções por
--connection-string "$AZURE_CONNECTION_STRING". O CORS é configurado uma vez para o serviço Blob da
conta e vale para todos os contêineres dela.
Um exemplo completo de Helm está em helm/studio/examples/values-azure.yaml.
Configurar o Google Cloud Storage
Criar os buckets
O GCS usa um bucket por finalidade, espelhando o layout do S3:
export PROJECT_ID=your-project-id
export LOCATION=us-central1
# Create buckets (names must be globally unique — prefix with your org)
for name in workspace-files knowledge-base execution-files chat-files \
copilot-files profile-pictures og-images workspace-logos; do
gcloud storage buckets create "gs://myorg-studio-$name" \
--project "$PROJECT_ID" \
--location "$LOCATION" \
--uniform-bucket-level-access
doneMantenha todos os buckets privados (sem bindings de allUsers). O Studio entrega os arquivos por URLs assinadas V4 de curta duração, então os buckets nunca precisam de leitura pública.
Como os uploads são enviados direto do navegador via requisições PUT assinadas, cada bucket precisa de uma política de CORS que permita a origem do seu Studio:
cat > /tmp/cors.json <<'EOF'
[
{
"origin": ["https://your-studio-domain.com"],
"method": ["GET", "PUT"],
"responseHeader": [
"Content-Type",
"ETag",
"x-goog-if-generation-match",
"x-goog-meta-uploadid",
"x-goog-meta-originalname",
"x-goog-meta-uploadedat",
"x-goog-meta-purpose",
"x-goog-meta-userid",
"x-goog-meta-workspaceid",
"x-goog-meta-knowledgebaseid",
"x-goog-meta-folderid",
"x-goog-meta-workflowid",
"x-goog-meta-executionid",
"x-goog-meta-simuploadid"
],
"maxAgeSeconds": 3600
}
]
EOF
for name in workspace-files knowledge-base execution-files chat-files \
copilot-files profile-pictures og-images workspace-logos; do
gcloud storage buckets update "gs://myorg-studio-$name" --cors-file=/tmp/cors.json
doneOs nomes dos headers precisam ser listados um a um — o CORS do GCS compara as entradas de responseHeader exatamente e não aceita curingas como x-goog-meta-*. O ETag é obrigatório porque uploads multipart de arquivos grandes leem o ETag de cada parte no navegador, e sem isso o CORS esconde o header. O x-goog-if-generation-match é exigido pelos uploads assinados somente-criação do Studio, que impedem que uma URL de upload reutilizada substitua bytes existentes. O x-goog-meta-simuploadid carrega o recibo opaco usado para verificar um upload depois de uma resposta de rede ambígua — a grafia é histórica e permanece como está, já que objetos que já existem no seu bucket a carregam.
Conceder acesso
Crie uma service account (ou reutilize aquela sob a qual sua carga de trabalho é executada) e conceda a ela acesso a objetos nos buckets:
gcloud iam service-accounts create studio-storage --project "$PROJECT_ID"
for name in workspace-files knowledge-base execution-files chat-files \
copilot-files profile-pictures og-images workspace-logos; do
gcloud storage buckets add-iam-policy-binding "gs://myorg-studio-$name" \
--member "serviceAccount:studio-storage@$PROJECT_ID.iam.gserviceaccount.com" \
--role roles/storage.objectAdmin
doneVocê tem então duas formas de fornecer as credenciais:
-
Application Default Credentials (recomendado no GCP) — execute o Studio com a service account via Workload Identity do GKE (ou associe-a à instância GCE) e deixe
GCS_CREDENTIALS_JSONsem valor. Como não há chave privada nesse modo, a geração de URLs assinadas usa a APIsignBlobdo IAM — conceda à service account a roleroles/iam.serviceAccountTokenCreatorsobre ela mesma:gcloud iam service-accounts add-iam-policy-binding \ "studio-storage@$PROJECT_ID.iam.gserviceaccount.com" \ --member "serviceAccount:studio-storage@$PROJECT_ID.iam.gserviceaccount.com" \ --role roles/iam.serviceAccountTokenCreator -
Chave inline (para Docker Compose ou hosts fora do GCP) — crie uma chave JSON para a service account e defina
GCS_CREDENTIALS_JSONcom o conteúdo dela. Com uma chave privada presente, as URLs assinadas são geradas localmente e nenhuma role IAM extra é necessária.
Configurar as variáveis de ambiente
# Credentials — omit both when using Workload Identity / ADC
GCS_PROJECT_ID=your-project-id # optional; inferred from credentials when unset
GCS_CREDENTIALS_JSON='{"type":"service_account","client_email":"...","private_key":"..."}'
# Buckets (per purpose)
GCS_BUCKET_NAME=myorg-studio-workspace-files
GCS_KB_BUCKET_NAME=myorg-studio-knowledge-base
GCS_EXECUTION_FILES_BUCKET_NAME=myorg-studio-execution-files
GCS_CHAT_BUCKET_NAME=myorg-studio-chat-files
GCS_COPILOT_BUCKET_NAME=myorg-studio-copilot-files
GCS_PROFILE_PICTURES_BUCKET_NAME=myorg-studio-profile-pictures
GCS_OG_IMAGES_BUCKET_NAME=myorg-studio-og-images
GCS_WORKSPACE_LOGOS_BUCKET_NAME=myorg-studio-workspace-logosSó GCS_BUCKET_NAME é estritamente obrigatória para colocar o Studio em modo GCS. Todo bucket por finalidade cai para o bucket geral quando não está definido — adicione os outros para que cada tipo de arquivo vá para o próprio bucket.
Referência dos buckets GCS
| Variável | Armazena | Obrigatória |
|---|---|---|
GCS_BUCKET_NAME | Arquivos gerais do workspace | Sim (ativa o GCS) |
GCS_PROJECT_ID | ID do projeto GCP | Não (inferido das credenciais/ADC) |
GCS_CREDENTIALS_JSON | JSON inline da service account | Não (usa Application Default Credentials se não definida) |
GCS_KB_BUCKET_NAME | Documentos da base de conhecimento | Recomendada (cai para GCS_BUCKET_NAME) |
GCS_EXECUTION_FILES_BUCKET_NAME | Arquivos de execução de workflow | Recomendada (cai para GCS_BUCKET_NAME) |
GCS_CHAT_BUCKET_NAME | Ativos do chat com deploy | Recomendada (cai para GCS_BUCKET_NAME) |
GCS_COPILOT_BUCKET_NAME | Anexos do Chat | Recomendada (cai para GCS_BUCKET_NAME) |
GCS_PROFILE_PICTURES_BUCKET_NAME | Avatares de pessoas | Recomendada (cai para GCS_BUCKET_NAME) |
GCS_OG_IMAGES_BUCKET_NAME | Imagens de preview OpenGraph (cai para GCS_BUCKET_NAME) | Opcional |
GCS_WORKSPACE_LOGOS_BUCKET_NAME | Logos de workspace (cai para GCS_BUCKET_NAME) | Opcional |
Um exemplo completo de Helm (Workload Identity, GKE) está em helm/studio/examples/values-gcp.yaml.
Configurar um provedor compatível com S3 (R2, MinIO, B2)
O Studio funciona com qualquer armazenamento compatível com S3 apontando o cliente S3 para um endpoint customizado. Configure exatamente como o AWS S3 (buckets, access key, secret) e depois adicione S3_ENDPOINT — e S3_FORCE_PATH_STYLE quando o provedor exigir endereçamento path-style. Verificado com Cloudflare R2, MinIO, Backblaze B2 e RustFS.
S3_ENDPOINT é configuração de operador confiável, então é usada como está — http:// e hosts privados são aceitos (sem barreira de SSRF/HTTPS). Não a conecte a entrada não confiável.
O endpoint precisa estar acessível pelos navegadores das pessoas, e o bucket precisa de CORS. Os uploads usam requisições PUT pré-assinadas enviadas direto do navegador para S3_ENDPOINT (os downloads voltam pelo app, então só precisam de acesso no servidor). Isso significa que:
- Um endpoint puramente interno (por exemplo
https://minio.internal:9000, que só os pods do app resolvem) deixa o servidor subir sem erro, mas os uploads falham no navegador. Use um endpoint que as pessoas consigam alcançar. - Configure uma política de CORS no bucket permitindo a origem do seu Studio (
PUT,GETe os headersAuthorization/Content-Type/x-amz-*). Isso vale também para o AWS S3 — R2 e MinIO não são diferentes.
O Cloudflare R2 usa o estilo virtual-hosted (o padrão) e a região auto:
AWS_REGION=auto
S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
AWS_ACCESS_KEY_ID=<r2-access-key-id>
AWS_SECRET_ACCESS_KEY=<r2-secret-access-key>
S3_BUCKET_NAME=myorg-studio-workspace-files
# ...remaining S3_*_BUCKET_NAME vars, one R2 bucket eachDeixe S3_FORCE_PATH_STYLE sem valor — o R2 suporta o endereçamento virtual-hosted padrão.
O MinIO (e o Ceph RGW) precisam de endereçamento path-style e aceitam qualquer string de região:
AWS_REGION=us-east-1
S3_ENDPOINT=https://minio.example.com # must be reachable from users' browsers, not app-pods-only
S3_FORCE_PATH_STYLE=true
AWS_ACCESS_KEY_ID=<minio-access-key>
AWS_SECRET_ACCESS_KEY=<minio-secret-key>
S3_BUCKET_NAME=myorg-studio-workspace-files
# ...remaining S3_*_BUCKET_NAME vars, one bucket eachhttp:// funciona do lado do servidor, mas como o navegador envia os uploads direto para esse endpoint, prefira um endpoint TLS acessível pelas pessoas (um destino http:// gera conteúdo misto e será bloqueado em uma origem https:// do Studio).
O RustFS é um armazenamento compatível com S3 escrito em Rust (um substituto direto do MinIO). Configure exatamente como o MinIO — path-style, qualquer string de região, access key/secret SigV4:
AWS_REGION=us-east-1
S3_ENDPOINT=https://rustfs.example.com # must be reachable from users' browsers
S3_FORCE_PATH_STYLE=true
AWS_ACCESS_KEY_ID=<rustfs-access-key>
AWS_SECRET_ACCESS_KEY=<rustfs-secret-key>
S3_BUCKET_NAME=myorg-studio-workspace-files
# ...remaining S3_*_BUCKET_NAME vars, one bucket eachValem os mesmos requisitos de acesso pelo navegador e de CORS.
Configurar a limpeza de multipart incompleto
O Studio envia direto para uma chave de objeto final somente-criação e mantém o estado da sessão de upload no PostgreSQL. O cron de limpeza reivindica as sessões expiradas antes de excluir um objeto enviado ou abortar o estado multipart no provedor. Configure a limpeza por ciclo de vida no provedor como segunda linha de defesa para estado multipart que sobrevive à sua linha no banco:
- No AWS S3 e no Google Cloud Storage, aborte uploads multipart incompletos depois de dois dias em cada bucket por finalidade.
- O Azure remove automaticamente blocos não commitados depois de sete dias.
- Em um provedor compatível com S3, configure a limpeza de multipart incompleto quando a implementação de ciclo de vida dele suportar. Confira a documentação do provedor, porque o suporte varia.
A janela do provedor deve ser maior que o tempo de vida de 24 horas da sessão de upload, para que uma conclusão em andamento ainda consiga se recuperar. Não adicione uma regra de expiração de objetos para as chaves de upload finais.
Expiração de objetos e limpeza de multipart incompleto são operações de ciclo de vida diferentes. Configure a operação de multipart incompleto; expirar objetos não remove partes multipart abandonadas.
Verificar se funciona
Depois de reiniciar com a nova configuração:
- Abra o app e envie um documento para uma base de conhecimento (ou defina uma foto de perfil).
- Confirme que um objeto aparece no bucket/contêiner correspondente.
- Recarregue a página — o arquivo deve continuar sendo exibido (os downloads voltam em stream pelo app em
/api/files/serve).
Se os uploads falharem, verifique os logs do app em busca de erros de credencial ou de permissões (veja Solução de problemas).