Object Storage

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:

BackendQuando usar
Disco localDocker em nó único, desenvolvimento local, avaliação
AWS S3Produção, especialmente com mais de uma réplica do app
Azure BlobProdução no Azure
Google Cloud StorageProduçã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:

  1. Azure Blob — usado se AZURE_STORAGE_CONTAINER_NAME estiver definida e também (AZURE_ACCOUNT_NAME + AZURE_ACCOUNT_KEY) ou AZURE_CONNECTION_STRING.
  2. AWS S3 — usado se S3_BUCKET_NAME e AWS_REGION estiverem definidas (e o Azure não estiver configurado).
  3. Google Cloud Storage — usado se GCS_BUCKET_NAME estiver definida (e nem o Azure nem o S3 estiverem configurados).
  4. 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"
done

Em 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
done

Defina 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_KEY sem 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-logos

Só 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ávelArmazenaObrigatória
AWS_REGIONRegião de todos os bucketsSim (ativa o S3)
AWS_ACCESS_KEY_IDAccess keyNão (usa a cadeia de credenciais se não definida)
AWS_SECRET_ACCESS_KEYSecret keyNão (usa a cadeia de credenciais se não definida)
S3_BUCKET_NAMEArquivos gerais do workspaceSim (ativa o S3)
S3_KB_BUCKET_NAMEDocumentos da base de conhecimentoRecomendada
S3_EXECUTION_FILES_BUCKET_NAMEArquivos de execução de workflow. Cai para o nome literal studio-execution-files, que você quase certamente não possui — sempre defina esta explicitamenteSim
S3_CHAT_BUCKET_NAMEAtivos do chat com deployRecomendada
S3_COPILOT_BUCKET_NAMEAnexos do ChatRecomendada
S3_PROFILE_PICTURES_BUCKET_NAMEAvatares de pessoasRecomendada
S3_OG_IMAGES_BUCKET_NAMEImagens de preview OpenGraph (cai para S3_BUCKET_NAME)Opcional
S3_WORKSPACE_LOGOS_BUCKET_NAMELogos de workspace (cai para S3_BUCKET_NAME)Opcional
S3_ENDPOINTEndpoint customizado para armazenamento compatível com S3 (R2, MinIO, B2)Opcional (AWS S3 se não definida)
S3_FORCE_PATH_STYLEtrue 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 -d

Como 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 buckets

No 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-logos

Uploads 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
done

Mantenha 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
done

Os 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
done

Você 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_JSON sem valor. Como não há chave privada nesse modo, a geração de URLs assinadas usa a API signBlob do IAM — conceda à service account a role roles/iam.serviceAccountTokenCreator sobre 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_JSON com 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-logos

Só 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ávelArmazenaObrigatória
GCS_BUCKET_NAMEArquivos gerais do workspaceSim (ativa o GCS)
GCS_PROJECT_IDID do projeto GCPNão (inferido das credenciais/ADC)
GCS_CREDENTIALS_JSONJSON inline da service accountNão (usa Application Default Credentials se não definida)
GCS_KB_BUCKET_NAMEDocumentos da base de conhecimentoRecomendada (cai para GCS_BUCKET_NAME)
GCS_EXECUTION_FILES_BUCKET_NAMEArquivos de execução de workflowRecomendada (cai para GCS_BUCKET_NAME)
GCS_CHAT_BUCKET_NAMEAtivos do chat com deployRecomendada (cai para GCS_BUCKET_NAME)
GCS_COPILOT_BUCKET_NAMEAnexos do ChatRecomendada (cai para GCS_BUCKET_NAME)
GCS_PROFILE_PICTURES_BUCKET_NAMEAvatares de pessoasRecomendada (cai para GCS_BUCKET_NAME)
GCS_OG_IMAGES_BUCKET_NAMEImagens de preview OpenGraph (cai para GCS_BUCKET_NAME)Opcional
GCS_WORKSPACE_LOGOS_BUCKET_NAMELogos 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, GET e os headers Authorization / 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 each

Deixe 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 each

http:// 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 each

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

  1. Abra o app e envie um documento para uma base de conhecimento (ou defina uma foto de perfil).
  2. Confirme que um objeto aparece no bucket/contêiner correspondente.
  3. 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).

Common Questions

Execute o Studio com o Workload Identity do GKE (ou uma service account associada à instância GCE) e deixe GCS_CREDENTIALS_JSON sem valor — as credenciais são resolvidas por Application Default Credentials. Conceda à service account a role roles/iam.serviceAccountTokenCreator sobre ela mesma para que as URLs assinadas possam ser geradas pela API signBlob do IAM.