Skip to content

Storage

Optional S3-compatible storage for document revisions, attachments and avatars, with the bundled SeaweedFS, Amazon S3 or Cloudflare R2.

Storage is optional. Without it, Orchestrat keeps documents in Postgres and they work normally, but attachments and avatar uploads are off. Add an S3-compatible bucket when you want them.

What storage holds

Everything goes in one private bucket.

DataWithout storageWith storage
Document revisionsStored in Postgres.Each new revision is also written to the bucket and read back from it, checked against its stored size and SHA-256.
AttachmentsOff. Uploads return 503 STORAGE_UPLOAD_FAILED.Files attached to documents: images you add in the dashboard editor and files agents upload with the CLI.
AvatarsOff. Uploads return 503 STORAGE_UPLOAD_FAILED.Profile pictures uploaded in onboarding and profile settings.

Objects live under three prefixes: document-revisions/v1/, document-attachments/v1/ and avatars/. Deleting a project deletes its revisions and attachments. Branch imports and merges copy attachment objects on the server.

Revisions written before you added storage stay in Postgres and keep working. Once storage is on, keep it on. See Turning storage off.

The bundled SeaweedFS

compose.storage.yml adds SeaweedFS, an S3-compatible server, on port 8333, and points Orchestrat at it. Start the stack with both files:

docker compose -f compose.yml -f compose.storage.yml up -d

It needs nothing beyond the .env that init wrote: S3_ACCESS_KEY and S3_SECRET_KEY. The overlay sets the server's ORCHESTRAT_S3_* values itself, including ORCHESTRAT_S3_PUBLIC_ENDPOINT=http://localhost:8333 and a bucket named orchestrat that the server creates on first start. The server waits for SeaweedFS to report healthy before it starts. To have browsers upload somewhere other than http://localhost:8333, set ORCHESTRAT_S3_PUBLIC_ENDPOINT in .env; only do that together with the overlay, because on its own it looks like half a storage configuration and the server refuses to start.

Pass both files to every later docker compose command, or a plain up -d recreates the server without storage. To make that automatic, add this line to .env:

COMPOSE_FILE=compose.yml:compose.storage.yml

SeaweedFS keeps its data in the seaweedfs Docker volume. Back it up with Postgres; see Upgrades and backups.

The bundled storage used to be MinIO. MinIO no longer publishes Docker images, so the compose files use SeaweedFS instead.

The public endpoint

Attachment bytes don't pass through the Orchestrat server on the way in. Browsers and the CLI upload straight to storage with presigned URLs:

  1. The dashboard or CLI asks the server for an upload. The server checks permissions and the project quota.
  2. The server returns a presigned PUT URL, valid for 15 minutes, that points at ORCHESTRAT_S3_PUBLIC_ENDPOINT.
  3. The client uploads the file to that URL. Files over 100 MiB are sent as a multipart upload in 16 MiB parts.
  4. The client tells the server the upload is complete, and the server checks the stored object's size.

Each attachment can be up to 2 GiB, and an unfinished upload can be resumed for 24 hours. Downloads go through the server, so the bucket never serves files to browsers directly.

So storage has two addresses:

  • ORCHESTRAT_S3_ENDPOINT is where the server reaches it. With the overlay, that's http://seaweedfs:8333 inside the compose network.
  • ORCHESTRAT_S3_PUBLIC_ENDPOINT is where browsers and agent machines reach it. With the overlay on your own machine, that's http://localhost:8333. When unset, it is the same as ORCHESTRAT_S3_ENDPOINT.

Every browser that uses the dashboard and every machine where agents run the CLI must be able to reach the public endpoint. On a server, give storage its own hostname, such as https://s3.orchestrat.example.com, and set ORCHESTRAT_S3_PUBLIC_ENDPOINT to it. A dashboard served over HTTPS needs an HTTPS storage endpoint, because browsers block uploads from an HTTPS page to plain HTTP. See HTTPS and reverse proxy.

Orchestrat uses path-style URLs by default: <endpoint>/<bucket>/<object key>. If you put a reverse proxy in front of storage, it must pass those paths and the Host header through unchanged, because the presigned signature covers them.

CORS

Because browsers upload straight to storage, storage must allow cross-origin PUT requests from your dashboard's origin, ORCHESTRAT_PUBLIC_URL, with a Content-Type header. No GET rule is needed, because downloads go through the server.

  • SeaweedFS allows any origin by default. There is nothing to set.
  • Amazon S3 and Cloudflare R2 need a CORS rule on the bucket:
[
  {
    "AllowedOrigins": ["https://orchestrat.example.com"],
    "AllowedMethods": ["PUT"],
    "AllowedHeaders": ["Content-Type"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]
  • Cloudflare R2: paste the rule into the bucket's Settings → CORS policy.
  • Amazon S3: paste it into the bucket's Permissions → Cross-origin resource sharing (CORS). With the AWS CLI, wrap the array as {"CORSRules": [...]} and run aws s3api put-bucket-cors --bucket orchestrat-example --cors-configuration file://cors.json.

If you change ORCHESTRAT_PUBLIC_URL, update the rule. Uploads from an origin that isn't listed fail in the browser before they reach the bucket.

Amazon S3

Don't use the overlay. Create a private bucket and an IAM user or role for Orchestrat, then set these in .env and run docker compose up -d:

ORCHESTRAT_S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
ORCHESTRAT_S3_REGION=us-east-1
ORCHESTRAT_S3_BUCKET=orchestrat-example
ORCHESTRAT_S3_ACCESS_KEY_ID=<access key ID>
ORCHESTRAT_S3_SECRET_ACCESS_KEY=<secret access key>
ORCHESTRAT_S3_PUBLIC_ENDPOINT=https://s3.us-east-1.amazonaws.com

The calls Orchestrat makes need this policy on the bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:ListBucketMultipartUploads"],
      "Resource": "arn:aws:s3:::orchestrat-example"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload",
        "s3:ListMultipartUploadParts"
      ],
      "Resource": "arn:aws:s3:::orchestrat-example/*"
    }
  ]
}

Cloudflare R2

Don't use the overlay. Create a bucket and an R2 API token with Object Read & Write on it, then set these in .env and run docker compose up -d:

ORCHESTRAT_S3_ENDPOINT=https://<account id>.r2.cloudflarestorage.com
ORCHESTRAT_S3_REGION=auto
ORCHESTRAT_S3_BUCKET=orchestrat
ORCHESTRAT_S3_ACCESS_KEY_ID=<R2 access key ID>
ORCHESTRAT_S3_SECRET_ACCESS_KEY=<R2 secret access key>
ORCHESTRAT_S3_PUBLIC_ENDPOINT=https://<account id>.r2.cloudflarestorage.com

R2's region is always auto. Use the account endpoint without the bucket name in the path, and set the bucket in ORCHESTRAT_S3_BUCKET. The server refuses to start when the endpoint includes a bucket path.

Avatars

Avatars (JPEG, PNG or WebP, up to 2 MiB) are uploaded through the Orchestrat server, which writes them to the bucket under avatars/. Browsers load them from the server too, at /api/v1/avatars/<key> on ORCHESTRAT_PUBLIC_URL, so the bucket stays private. Each key is random, and anyone who has an avatar's URL can load it.

Attachment quota

Each project can store up to ORCHESTRAT_ATTACHMENT_QUOTA_BYTES of attachments, 10 GiB by default. The total counts finished attachments, uploads in progress and copies made by branch imports and merges. An upload that would go over the limit fails with ATTACHMENT_QUOTA_EXCEEDED.

# 50 GiB per project
ORCHESTRAT_ATTACHMENT_QUOTA_BYTES=53687091200

Turning storage off

If you remove the storage settings after storage has been used, the server still starts, and logs a warning with the number of document revisions and attachments it can no longer reach. Those revisions return 503 DOCUMENT_CONTENT_UNAVAILABLE and those attachments 503 STORAGE_UPLOAD_FAILED until you set the same storage settings again. Nothing is deleted.

Old variable names

Earlier versions read DOCUMENT_STORAGE_S3_ENDPOINT, DOCUMENT_STORAGE_S3_REGION, DOCUMENT_STORAGE_BUCKET, DOCUMENT_STORAGE_S3_ACCESS_KEY_ID and DOCUMENT_STORAGE_S3_SECRET_ACCESS_KEY. They still work when the matching ORCHESTRAT_S3_* variable is unset, and the server logs a warning asking you to rename them.

Back up the bucket

Once storage is on, a Postgres backup alone is not a complete backup: newer document revisions, every attachment and every avatar live in the bucket. Back up the bucket together with the database, and restore them together. See Upgrades and backups.