Sim stores every uploaded file — knowledge base documents, chat attachments, execution outputs, profile pictures, and more — in object storage. Four backends are supported:
| Backend | When to use |
|---|---|
| Local disk | Single-node Docker, local development, evaluation |
| AWS S3 | Production, especially when running more than one app replica |
| Azure Blob | Production on Azure |
| Google Cloud Storage | Production on GCP |
Local disk writes to /app/apps/sim/uploads inside the container. Neither docker-compose.prod.yml nor the Helm chart mounts a volume there, so every uploaded file is lost the moment the container is recreated — and files are not shared across replicas. Mount your own volume at that path if you must stay on local disk; for any multi-replica or production deployment, use S3, Azure Blob, or Google Cloud Storage.
How the backend is selected
Set STORAGE_PROVIDER to local, s3, azure, or gcs to select a backend explicitly. When it is unset, Sim infers the backend from the configured environment variables in this order:
- Azure Blob — used if
AZURE_STORAGE_CONTAINER_NAMEis set and either (AZURE_ACCOUNT_NAME+AZURE_ACCOUNT_KEY) orAZURE_CONNECTION_STRINGis set. - AWS S3 — used if
S3_BUCKET_NAMEandAWS_REGIONare set (and Azure is not configured). - Google Cloud Storage — used if
GCS_BUCKET_NAMEis set (and neither Azure nor S3 is configured). - Local disk — used only when no cloud backend has been activated. Setting any of a backend's keys activates it, including a dedicated bucket,
S3_ENDPOINT, or an Azure credential — so a half-configured backend fails at startup rather than quietly falling back here.
If STORAGE_PROVIDER is unset, Sim uses the first backend whose configuration is complete, in that order. An explicit STORAGE_PROVIDER takes precedence and must be valid and complete.
A partially configured backend is skipped, so a fully configured one later in the order still wins. A backend whose values are present but malformed is different: it stops startup immediately, and a later fully configured one does not take over. What it never does is fall back to local disk: if no other backend is complete, startup fails with File storage is partially or incorrectly configured. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are checked as a pair — set both or neither.
Set up AWS S3
Create the buckets
Sim separates files into purpose-specific buckets. Sim never creates buckets — create each one yourself before configuring it. Most S3 bucket variables do not fall back to S3_BUCKET_NAME: leave S3_KB_BUCKET_NAME, S3_CHAT_BUCKET_NAME, S3_COPILOT_BUCKET_NAME, or S3_PROFILE_PICTURES_BUCKET_NAME unset and it resolves to an empty bucket name, so that file type fails to store. Three behave differently — S3_OG_IMAGES_BUCKET_NAME and S3_WORKSPACE_LOGOS_BUCKET_NAME fall back to S3_BUCKET_NAME, and S3_EXECUTION_FILES_BUCKET_NAME falls back to a literal default name (see the reference table below).
# Set your region once
export AWS_REGION=us-east-1
# The eight purpose-specific buckets. The CORS and IAM steps below reuse this list.
export SIM_BUCKETS="workspace-files knowledge-base execution-files chat-files
copilot-files profile-pictures og-images workspace-logos"
# Create buckets (names must be globally unique — prefix with your org)
for name in ${SIM_BUCKETS:?run the export above first}; do
aws s3api create-bucket \
--bucket "myorg-sim-$name" \
--region "$AWS_REGION" \
--create-bucket-configuration LocationConstraint="$AWS_REGION"
doneIn us-east-1, omit the --create-bucket-configuration flag — that region rejects an explicit LocationConstraint.
Keep all buckets private (block public access). Sim serves files through short-lived presigned URLs, so the buckets never need public read access.
Configure CORS on every bucket
Uploads are sent directly from the browser to S3 via presigned PUT requests, so each bucket needs a CORS policy that allows your Sim origin. Without this, every upload fails with a CORS error in the browser console even though the server-side configuration is correct.
cat > /tmp/cors.json <<'EOF'
{
"CORSRules": [
{
"AllowedOrigins": ["https://sim.yourdomain.com"],
"AllowedMethods": ["GET", "PUT"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}
]
}
EOF
for name in ${SIM_BUCKETS:?run the export above first}; do
aws s3api put-bucket-cors --bucket "myorg-sim-$name" --cors-configuration file:///tmp/cors.json
doneSet AllowedOrigins to your exact Sim origin (scheme + host, no trailing slash). Add every origin users reach Sim from, including an apex/www pair if both are live.
Grant access with an IAM policy
Create an IAM policy scoped to your buckets and attach it to the user (or role) Sim runs as:
{
"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-sim-*",
"arn:aws:s3:::myorg-sim-*/*"
]
}
]
}You then have two ways to supply credentials:
- Static keys — create an IAM user with this policy and set
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY. - Instance/role credentials (recommended) — attach the policy to the EC2 instance role, ECS task role, or EKS IRSA role. Leave
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYunset and Sim falls back to the default AWS credential chain automatically.
Configure environment variables
Set the region, optionally the credentials, and the bucket names:
# 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-sim-workspace-files
S3_KB_BUCKET_NAME=myorg-sim-knowledge-base
S3_EXECUTION_FILES_BUCKET_NAME=myorg-sim-execution-files
S3_CHAT_BUCKET_NAME=myorg-sim-chat-files
S3_COPILOT_BUCKET_NAME=myorg-sim-copilot-files
S3_PROFILE_PICTURES_BUCKET_NAME=myorg-sim-profile-pictures
S3_OG_IMAGES_BUCKET_NAME=myorg-sim-og-images
S3_WORKSPACE_LOGOS_BUCKET_NAME=myorg-sim-workspace-logosAWS_REGION and S3_BUCKET_NAME are what switch Sim into S3 mode, but they do not cover the other file types — set every bucket above, because only the OpenGraph and workspace-logo buckets fall back to the general one.
S3 bucket reference
| Variable | Stores | Required |
|---|---|---|
AWS_REGION | Region for all buckets | Yes (enables S3) |
AWS_ACCESS_KEY_ID | Access key | No (uses credential chain if unset) |
AWS_SECRET_ACCESS_KEY | Secret key | No (uses credential chain if unset) |
S3_BUCKET_NAME | General workspace files | Yes (enables S3) |
S3_KB_BUCKET_NAME | Knowledge base documents | Yes, to use knowledge bases (no fallback) |
S3_EXECUTION_FILES_BUCKET_NAME | Workflow execution files. Falls back to the literal name sim-execution-files, which you almost certainly do not own — always set this explicitly | Effectively yes — the fallback is a bucket you do not own |
S3_CHAT_BUCKET_NAME | Deployed chat assets | Yes, to use deployed chats (no fallback) |
S3_COPILOT_BUCKET_NAME | Chat attachments | Yes, to attach files in Chat (no fallback) |
S3_PROFILE_PICTURES_BUCKET_NAME | User avatars | Yes, for avatar uploads (no fallback) |
S3_OG_IMAGES_BUCKET_NAME | OpenGraph preview images | Optional (falls back to S3_BUCKET_NAME) |
S3_WORKSPACE_LOGOS_BUCKET_NAME | Workspace logos | Optional (falls back to S3_BUCKET_NAME) |
S3_ENDPOINT | Custom endpoint for S3-compatible storage (R2, MinIO, B2) | Optional (AWS S3 if unset) |
S3_FORCE_PATH_STYLE | true for path-style addressing (MinIO/Ceph) | Optional (defaults false) |
Apply the configuration
Add the storage variables to the .env file used by docker-compose.prod.yml, then restart:
docker compose -f docker-compose.prod.yml up -dNew uploads now use S3. Preserve any existing local files until you have migrated them; changing the storage configuration does not copy their bytes.
Set the variables under app.env (non-secret, e.g. region and bucket names) and supply credentials through a secret. The chart ships a complete example at helm/sim/examples/values-aws.yaml:
app:
env:
AWS_REGION: "us-east-1"
S3_BUCKET_NAME: "myorg-sim-workspace-files"
S3_KB_BUCKET_NAME: "myorg-sim-knowledge-base"
S3_EXECUTION_FILES_BUCKET_NAME: "myorg-sim-execution-files"
# ...remaining bucketsOn EKS, prefer IRSA: attach the IAM policy to the service account's role and leave the access-key variables unset.
Set up Azure Blob
Azure Blob uses one container per purpose, mirroring the S3 layout. Authenticate with either a connection string or an account name + key.
# 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-logosSet every container name. Like the S3 variables, the knowledge-base, chat, Chat-attachment, and profile-picture containers do not fall back to AZURE_STORAGE_CONTAINER_NAME — an unset one resolves to an empty container name and that file type fails to store. The OpenGraph and workspace-logo containers do fall back to it, and AZURE_STORAGE_EXECUTION_FILES_CONTAINER_NAME falls back to the literal name sim-execution-files.
Direct browser uploads require a Blob service CORS rule on the storage account. Allow your exact
Sim origin, GET and PUT, the Content-Type header, and the x-ms-* prefix used by signed blob
and metadata headers. Small-file uploads also send If-None-Match; the signature itself is
create-only, so a signed URL cannot overwrite an existing final object. Multipart completion verifies uploaded parts on the server:
az storage cors add \
--services b \
--methods GET PUT OPTIONS \
--origins https://sim.yourdomain.com \
--allowed-headers content-type if-none-match 'x-ms-*' \
--exposed-headers ETag \
--max-age 3600 \
--account-name mystorageaccount \
--account-key '<account-key>'If you authenticate with a connection string, replace the last two options with
--connection-string "$AZURE_CONNECTION_STRING". CORS is configured once for the account's Blob
service and applies to all of its containers.
A full Helm example lives at helm/sim/examples/values-azure.yaml.
Set up Google Cloud Storage
Create the buckets
GCS uses one bucket per purpose, mirroring the S3 layout:
export PROJECT_ID=your-project-id
export LOCATION=us-central1
# The eight purpose-specific buckets. The CORS and IAM steps below reuse this list.
export SIM_BUCKETS="workspace-files knowledge-base execution-files chat-files
copilot-files profile-pictures og-images workspace-logos"
# Create buckets (names must be globally unique — prefix with your org)
for name in ${SIM_BUCKETS:?run the export above first}; do
gcloud storage buckets create "gs://myorg-sim-$name" \
--project "$PROJECT_ID" \
--location "$LOCATION" \
--uniform-bucket-level-access
doneKeep all buckets private (no allUsers bindings). Sim serves files through short-lived V4 signed URLs, so the buckets never need public read access.
Because uploads are sent directly from the browser via signed PUT requests, each bucket needs a CORS policy that allows your Sim origin:
cat > /tmp/cors.json <<'EOF'
[
{
"origin": ["https://your-sim-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"
],
"maxAgeSeconds": 3600
}
]
EOF
for name in ${SIM_BUCKETS:?run the export above first}; do
gcloud storage buckets update "gs://myorg-sim-$name" --cors-file=/tmp/cors.json
doneHeader names must be listed individually — GCS CORS matches responseHeader entries exactly and does not support wildcards like x-goog-meta-*. The example exposes ETag, although the current upload-session client does not need it: multipart completion reads provider-authoritative part state on the server. x-goog-if-generation-match is required by Sim's create-only signed uploads, which prevent a reused upload URL from replacing existing bytes.
Grant access
Create a service account (or reuse the one your workload runs as) and grant it object access on the buckets:
gcloud iam service-accounts create sim-storage --project "$PROJECT_ID"
for name in ${SIM_BUCKETS:?run the export above first}; do
gcloud storage buckets add-iam-policy-binding "gs://myorg-sim-$name" \
--member "serviceAccount:sim-storage@$PROJECT_ID.iam.gserviceaccount.com" \
--role roles/storage.objectAdmin
doneYou then have two ways to supply credentials:
-
Application Default Credentials (recommended on GCP) — run Sim with the service account via GKE Workload Identity (or attach it to the GCE instance) and leave
GCS_CREDENTIALS_JSONunset. Because there is no private key in this mode, generating signed URLs uses the IAMsignBlobAPI — grant the service accountroles/iam.serviceAccountTokenCreatoron itself:gcloud iam service-accounts add-iam-policy-binding \ "sim-storage@$PROJECT_ID.iam.gserviceaccount.com" \ --member "serviceAccount:sim-storage@$PROJECT_ID.iam.gserviceaccount.com" \ --role roles/iam.serviceAccountTokenCreator -
Inline key (for Docker Compose or non-GCP hosts) — create a JSON key for the service account and set
GCS_CREDENTIALS_JSONto its contents. With a private key present, signed URLs are generated locally and no extra IAM role is needed.
Configure environment variables
# 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-sim-workspace-files
GCS_KB_BUCKET_NAME=myorg-sim-knowledge-base
GCS_EXECUTION_FILES_BUCKET_NAME=myorg-sim-execution-files
GCS_CHAT_BUCKET_NAME=myorg-sim-chat-files
GCS_COPILOT_BUCKET_NAME=myorg-sim-copilot-files
GCS_PROFILE_PICTURES_BUCKET_NAME=myorg-sim-profile-pictures
GCS_OG_IMAGES_BUCKET_NAME=myorg-sim-og-images
GCS_WORKSPACE_LOGOS_BUCKET_NAME=myorg-sim-workspace-logosGCS_BUCKET_NAME is what switches Sim into GCS mode and is the only bucket strictly required: every purpose-specific bucket falls back to it when unset. GCS bucket names are globally unique, so Sim uses no literal defaults here — not even for execution files. Set the dedicated buckets to separate the file types; leave them unset and everything lands in the general bucket.
GCS bucket reference
| Variable | Stores | Required |
|---|---|---|
GCS_BUCKET_NAME | General workspace files | Yes (enables GCS) |
GCS_PROJECT_ID | GCP project ID | No (inferred from credentials/ADC) |
GCS_CREDENTIALS_JSON | Inline service-account JSON | No (uses Application Default Credentials if unset) |
GCS_KB_BUCKET_NAME | Knowledge base documents | Recommended (falls back to GCS_BUCKET_NAME) |
GCS_EXECUTION_FILES_BUCKET_NAME | Workflow execution files | Recommended (falls back to GCS_BUCKET_NAME) |
GCS_CHAT_BUCKET_NAME | Deployed chat assets | Recommended (falls back to GCS_BUCKET_NAME) |
GCS_COPILOT_BUCKET_NAME | Chat attachments | Recommended (falls back to GCS_BUCKET_NAME) |
GCS_PROFILE_PICTURES_BUCKET_NAME | User avatars | Recommended (falls back to GCS_BUCKET_NAME) |
GCS_OG_IMAGES_BUCKET_NAME | OpenGraph preview images | Recommended (falls back to GCS_BUCKET_NAME) |
GCS_WORKSPACE_LOGOS_BUCKET_NAME | Workspace logos | Recommended (falls back to GCS_BUCKET_NAME) |
A full Helm example (Workload Identity, GKE) lives at helm/sim/examples/values-gcp.yaml.
Set up an S3-compatible provider (R2, MinIO, B2)
Sim works with any S3-compatible store by pointing the S3 client at a custom endpoint. Configure it exactly like AWS S3 (buckets, access key, secret), then add S3_ENDPOINT — and S3_FORCE_PATH_STYLE where the provider requires path-style addressing. Verified with Cloudflare R2, MinIO, Backblaze B2, and RustFS.
S3_ENDPOINT is trusted operator configuration, so it is used as-is — http:// and private hosts are accepted (no SSRF/HTTPS gate). Don't wire it to untrusted input.
The endpoint must be reachable from your users' browsers, and the bucket needs CORS. Uploads use presigned PUT requests sent directly from the browser to S3_ENDPOINT (downloads are proxied back through the app, so they only need server-side reachability). This means:
- A purely internal endpoint (e.g.
https://minio.internal:9000that only the app pods can resolve) will let the server start cleanly but uploads will fail in the browser. Use an endpoint your users can reach. - Configure a CORS policy on the bucket that allows your Sim origin (
PUT,GET, and theAuthorization/Content-Type/x-amz-*headers). This applies to AWS S3 too — R2 and MinIO are no different.
Cloudflare R2 uses virtual-hosted style (the default) and the region 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-sim-workspace-files
# ...remaining S3_*_BUCKET_NAME vars, one R2 bucket eachLeave S3_FORCE_PATH_STYLE unset — R2 supports the default virtual-hosted addressing.
MinIO (and Ceph RGW) need path-style addressing and accept any region string:
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-sim-workspace-files
# ...remaining S3_*_BUCKET_NAME vars, one bucket eachhttp:// works server-side, but since the browser uploads directly to this endpoint, prefer a TLS endpoint your users can reach (a mixed-content http:// target will be blocked on an https:// Sim origin).
RustFS is a Rust-based, S3-compatible store (a MinIO drop-in). Configure it exactly like MinIO — path-style, any region string, SigV4 access key/secret:
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-sim-workspace-files
# ...remaining S3_*_BUCKET_NAME vars, one bucket eachThe same browser-reachability and CORS requirements apply.
Configure incomplete multipart cleanup
Sim uploads directly to a create-only final object key and keeps upload-session state in PostgreSQL. The cleanup cron claims expired sessions before deleting an uploaded object or aborting its provider multipart state. Configure provider lifecycle cleanup as a second line of defense for multipart state that outlives its database row:
- On AWS S3 and Google Cloud Storage, abort incomplete multipart uploads after two days on every purpose-specific bucket.
- Azure automatically removes uncommitted blocks after seven days.
- For an S3-compatible provider, configure incomplete-multipart cleanup when its lifecycle implementation supports it. Check the provider's documentation because support varies.
The provider window should exceed the 24-hour upload-session lifetime so an in-progress completion can still recover. Do not add an object-expiration rule for final upload keys.
Object expiration and incomplete-multipart cleanup are different lifecycle operations. Configure the incomplete-multipart operation; expiring objects does not remove abandoned multipart parts.
Verify it works
After restarting with the new configuration:
- Open the app and upload a document to a knowledge base (or set a profile picture).
- Confirm an object appears in the corresponding bucket/container.
- Reload the page — the file should still render (downloads stream back through the app at
/api/files/serve).
If uploads fail, check the app logs for credential or permission errors (see Troubleshooting).