Pick a backend
Local filesystem
Set the path where PostQueen should write:Who actually serves the files
Not the backend, which is the part most people get wrong when they mount the volume.Either way the frontend is what reads the directory. Mount the
volume where the frontend can see it, and on the official image leave
UPLOAD_DIRECTORY at /uploads, because that is the path nginx is
compiled to serve.Docker volume mount
Indocker-compose.yaml:
If you scale beyond one backend replica, you need a shared volume, or
switch to Cloudflare R2.
Caveat: some providers need public HTTPS URLs
If your deployment is internet-facing through a reverse proxy with TLS, you are fine. If PostQueen is on a private network, those providers will fail and you should use Cloudflare R2 or a CDN instead.Cloudflare R2
SetSTORAGE_PROVIDER=cloudflare and configure the R2 credentials. See
the dedicated R2 setup guide for the OAuth and
bucket-permissions walkthrough.
Two ways an upload can travel
By default the browser sends the file straight to the bucket, using a URL the backend signs for it. The file never passes through your server: WithUPLOAD_VIA_SERVER=true the file is posted to PostQueen, which writes it
to the bucket itself:
The next section is what “interfere” looks like in practice.
Either way the size limit is the same, 1 GB per file, enforced while the
upload is being read rather than after it finishes. What a social network will
publish is far smaller and decided by the network, not by this.
When uploads fail with a CORS error
With R2, the browser uploads to the bucket directly using a URL the backend signs for it. That signature covers the URL’s query parameters, so if anything alters the request between the browser and R2, whether a privacy extension stripping what it takes for tracking parameters or a corporate proxy, the signature no longer matches and R2 answers403.
R2 leaves CORS headers off its error responses, so the browser cannot read that
403 and reports it as:
The tell is that the backend’s own requests all succeed:
create-multipart-upload and sign-part return 200, then
abort-multipart-upload follows after a few retries.Setting
UPLOAD_VIA_SERVER=true sidesteps it. Files are posted to the backend,
which writes them to R2 itself, so nothing between the browser and the bucket
can invalidate anything. Uploads are spooled to disk rather than held in memory
and deleted once stored, so a large video does not become a memory spike.Serving media from your own domain
PointCLOUDFLARE_BUCKET_URL at your own host and PostQueen serves the media
itself:
Note the
/api prefix. nginx inside the container maps /uploads/ to a local
directory, so only /api/uploads reaches the backend.Public-API uploads
Both/public/v1/upload and /public/v1/upload-from-url write through
the configured STORAGE_PROVIDER. The accepted MIME types and body-size
limits are documented in troubleshooting/uploads.
Next steps
Set up Cloudflare R2
The bucket, the token, the CORS policy and the three ways media can be read back
When an upload fails
Payload limits, video failures and the size limit that catches people out
Configuration reference
Every environment variable she reads, and why a plain restart is not enough
Backups and upgrades
Which volumes carry real data, and what changes once media lives in a bucket