Skip to content

Object storage for data exports

EXPORT_STORAGE is local by default, which writes archives under var/exports/. That is only correct when the process that generates an export and the process that serves its download share a filesystem. The single-host stack does share one; separate web and worker containers do not, and there local means the worker writes an archive the web container cannot see and every download 404s.

VariablePurpose
EXPORT_STORAGElocal or s3.
EXPORT_STORAGE_BUCKETRequired when s3.
EXPORT_STORAGE_PREFIXKey prefix. Empty stores archives at the bucket root, which is what a dedicated bucket wants.
EXPORT_STORAGE_REGIONEmpty falls back to AWS’s default, us-east-1.
EXPORT_STORAGE_ENDPOINTSet for any non-AWS provider, e.g. https://tor1.digitaloceanspaces.com. Empty targets AWS S3 itself.
EXPORT_STORAGE_KEY / _SECRETEmpty falls back to the ambient AWS credential chain, which only helps when running on AWS with an attached role.
EXPORT_STORAGE_USE_PATH_STYLEtrue for MinIO and most non-AWS providers, which address buckets as https://host/bucket/key rather than https://bucket.host/key.
EXPORT_STORAGE_ACLCanned ACL sent with every upload. No single value works everywhere — see Known gaps.

AWS S3, MinIO, Cloudflare R2 and DigitalOcean Spaces all work; the application only ever sees generic S3 settings. Nothing else in the app writes files, so this is the only place object storage is needed.

Keeping archives private on a bring-your-own bucket

Export archives are personal data, and the application never exposes the bucket to a browser. It writes every object with a private ACL and private Flysystem visibility, and it generates no public and no presigned URL — the download route streams the bytes itself, behind a link that requires the authenticated owner plus a SHA-256 token, expires 48 hours after the export completes, and answers 404 on any mismatch.

On an AWS S3 bucket created since 2023, that private object ACL is a no-op. Those buckets default to “Bucket owner enforced” ownership, under which S3 ignores object ACLs entirely and access is governed solely by Block Public Access and the bucket policy. It is the same setting that forces EXPORT_STORAGE_ACL=bucket-owner-full-control, so it applies to exactly the buckets that need the override. Enable Block Public Access and grant no anonymous read in the bucket policy — on those buckets the application cannot do it for you.

This is already handled when create_export_bucket = true: terraform/spaces.tf creates the bucket private with a key scoped to it alone, and Spaces honours that bucket ACL. Nothing to do on the shipped DigitalOcean path.