Platform CLI
Learn
Developer docs User guide Quickstart CLI and AI agents Blog
Company
Services About Contact Links Get started

File storage

Updated

Raytha keeps uploaded files, and the assets of your themes, in a file storage provider. Pick one with FILE_STORAGE_PROVIDER: Local (the default), AzureBlob or S3.

What goes into file storage

  • Everything in the media library, and files uploaded from content fields and the editor.
  • The assets of a theme (CSS, JavaScript, fonts, images). The default theme's assets are written there during setup.

The database holds only a row per file (name, size, type, object key). The bytes are in the provider, so a database backup does not contain them. See Backups and restores.

Each file gets an object key made from its id and a cleaned file name, for example lNGmEiOqJEWlclH34pAFxg_hello_world.txt. Content refers to files by a root-relative URL, /raytha/media-items/objectkey/<key>. That endpoint is public and redirects to the file: to /_static-files/<key> for Local, or to a signed URL that expires after one day for Azure and S3. Because of the redirect, the bucket or container can stay private.

Settings that apply to every provider

VariableDefaultNotes
FILE_STORAGE_PROVIDERLocalCase-insensitive. Fixed at start-up.
FILE_STORAGE_ALLOWED_MIMETYPEStext/*,image/*,video/*,audio/*,application/pdfChecked on the server for every upload path, including cloud uploads.
FILE_STORAGE_MAX_FILE_SIZE20000000Bytes. Enforced for local uploads, the admin uploader and CSV import. A browser that uploads directly to a bucket is not size-checked by Raytha; set a size limit at the bucket or CDN if that matters.
FILE_STORAGE_MAX_TOTAL_DISK_SPACE1000000000Bytes. Only displayed on the dashboard; nothing stops uploads at this size.
FILE_STORAGE_USE_DIRECT_UPLOAD_TO_CLOUDtrueCloud providers only. true: the browser uploads straight to the bucket with a signed URL. false: the file passes through Raytha. Ignored (always off) for Local.

Local disk

FILE_STORAGE_PROVIDER=Local
FILE_STORAGE_LOCAL_DIRECTORY=user-uploads

Files are written to FILE_STORAGE_LOCAL_DIRECTORY, created at start-up if missing, and served at /_static-files. Served files carry a restrictive Content-Security-Policy (sandbox) so an uploaded SVG cannot run scripts.

The relative default resolves against the app's working directory, which is /app in Docker. Mount a volume there so files outlive the container:

services:
  app:
    volumes:
      - raytha_user_uploads:/app/user-uploads

The repository's docker-compose.yml already does this. Local storage works for a single instance. Two app instances would not share the directory.

Warning Raytha lower-cases FILE_STORAGE_LOCAL_DIRECTORY when it writes files and reads health, but not when it serves them. A directory such as /data/Uploads makes uploads fail and /healthz/ready report "Local storage directory does not exist." Use a lower-case path.

Azure Blob Storage

FILE_STORAGE_PROVIDER=AzureBlob
FILE_STORAGE_AZUREBLOB_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=...;EndpointSuffix=core.windows.net
FILE_STORAGE_AZUREBLOB_CONTAINER=raytha-media
# Optional: replace the storage host in generated URLs (a CDN or custom domain).
FILE_STORAGE_AZUREBLOB_CUSTOM_DOMAIN=media.example.com
  • The connection string and container name are both required. If either is empty Raytha throws "Azure Environment Variables were not found" the first time it touches storage.
  • Create the container yourself. Raytha does not create it.
  • Uploads use a write-only signed URL and downloads a read-only one, both generated with the account key in the connection string.
  • FILE_STORAGE_AZUREBLOB_CUSTOM_DOMAIN only swaps the host in generated URLs. The domain must already reach your container, for example through a CDN with the container as its origin.

With direct upload on, the browser sends a PUT to the container with an x-ms-blob-type: BlockBlob header, so the storage account needs a CORS rule for your site's origin. With the Azure CLI:

az storage cors add --services b \
  --methods GET HEAD PUT \
  --origins https://www.example.com \
  --allowed-headers '*' --exposed-headers '*' --max-age 3600 \
  --connection-string "$AZURE_STORAGE_CONNECTION_STRING"

S3 and S3-compatible storage

FILE_STORAGE_PROVIDER=S3
FILE_STORAGE_S3_ACCESS_KEY=AKIA...
FILE_STORAGE_S3_SECRET_KEY=...
FILE_STORAGE_S3_BUCKET=raytha-media
FILE_STORAGE_S3_SERVICE_URL=https://s3.us-east-1.amazonaws.com
FILE_STORAGE_S3_REGION=us-east-1
  • The access key, secret key, bucket and service URL are all required, for AWS too. Without any one of them Raytha throws "S3 Environment Variables were not found". The comments in .env.example say AWS does not need the service URL; the code says otherwise.
  • Raytha always uses path-style addressing (https://host/bucket/key), which every S3-compatible service supports.
  • For AWS, use the regional endpoint for the bucket's region and set FILE_STORAGE_S3_REGION to match. The region defaults to us-east-1.
  • MinIO, Cloudflare R2 and similar services: set the service URL to their endpoint. Raytha's development stack runs MinIO this way.
  • An http:// service URL is accepted only while direct upload is on (the default). With direct upload off, the service URL must be https:// or Raytha refuses to start the provider.
  • Create the bucket yourself. Raytha does not create it, and the credentials need permission to put, get and delete objects.

With direct upload on, the browser sends a PUT with Content-Type and x-ms-blob-type headers to the signed URL, so the bucket needs a CORS rule for your site's origin. In the format used by the AWS console and aws s3api put-bucket-cors:

{
  "CORSRules": [
    {
      "AllowedOrigins": ["https://www.example.com"],
      "AllowedMethods": ["GET", "HEAD", "PUT"],
      "AllowedHeaders": ["*"],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 3600
    }
  ]
}

Check that it works

  1. Open /healthz/ready. The storage check should be Healthy. For Local it writes and deletes a probe file. For Azure and S3 it only checks that the provider can be built and a signed URL generated, so it does not prove the credentials or CORS rule are right.
  2. Upload a file in Media and open it.
  3. If the upload fails in the browser console with a CORS error, fix the bucket's CORS rule. If it fails with a 403, check the credentials and permissions.

Switching providers

Changing FILE_STORAGE_PROVIDER does not move files. Raytha builds download URLs from the object key using whichever provider is active, so to switch without breaking links, copy every object to the new provider under the same key first, then change the setting and restart. Do not forget the theme assets.

Gotchas

  • Without a volume (or a cloud provider), uploads are lost when the container is replaced.
  • An upload whose type is empty is rejected. A type/* entry matches by prefix, and exact entries match without regard to case.
  • The two "max" variables that look like quotas, FILE_STORAGE_MAX_TOTAL_DISK_SPACE and DATABASE_MAX_SIZE, do not block anything.
  • The media redirect endpoint is public. Anyone who knows an object key can fetch the file, so do not rely on obscure names for privacy.

Next steps