Media Storage
This page is for operators choosing local or S3-compatible media storage.
OpenPost stores media on the local file system by default. It can also use S3 or R2 storage.
Video processing dependency
OpenPost requires ffmpeg and ffprobe on PATH to verify uploaded videos and create posters. The official Docker image includes both tools, and the project Devenv supplies them for development. Install FFmpeg separately when running the standalone binary. A video stays unavailable for publishing if the server cannot complete this verification; Media shows the processing error and offers a retry after the dependency or file problem is fixed.
Key settings
OPENPOST_MEDIA_PATHcontrols where files are stored on disk.OPENPOST_MEDIA_URLcontrols how those files are exposed publicly. Its default/mediapath is resolved againstOPENPOST_PUBLIC_URL, which falls back toOPENPOST_APP_URL.OPENPOST_STORAGE_DRIVERchooseslocalors3.
Recommended production values
OPENPOST_MEDIA_PATH=/data/media
OPENPOST_APP_URL=https://openpost.example.com
OPENPOST_PUBLIC_URL=https://openpost.example.comThe default OPENPOST_MEDIA_URL=/media becomes https://openpost.example.com/media. Set an absolute OPENPOST_MEDIA_URL when media uses a separate domain or path.
Why public media URLs matter
Threads, Facebook, Instagram, and some TikTok posts need a public HTTPS link to the media. Those posts will fail if the social network cannot open the file.
Backups
Back up the media directory together with the SQLite database when using local storage. For S3/R2-style storage, back up the bucket or configure provider-side versioning and lifecycle protection.
Lifecycle cleanup
OpenPost applies a fixed application-level lifecycle to local and S3-compatible storage:
- Post-specific temporary media moves to Trash after its final successful publication or 14 days without use.
- Trash remains recoverable for seven days, then becomes eligible for permanent removal.
- Favorites, tags, collections, brand files, active posts and publications, retryable work, source relationships, and live editor projects protect their media from automatic cleanup.
These periods are not workspace or environment settings. The deprecated media_cleanup_days API field remains only for old clients: reads return 14 and writes are ignored. Old queued cleanup jobs that contain a days value also use the fixed 14-day policy.
Each cleanup run computes protection once for a bounded database batch. OpenPost commits database changes before it asks local or remote object storage to delete bytes, so a slow storage service does not keep a SQLite or PostgreSQL transaction open.
S3-compatible storage
Use these settings for S3/R2-style storage:
OPENPOST_STORAGE_DRIVER=s3
OPENPOST_S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com
OPENPOST_S3_REGION=auto
OPENPOST_S3_BUCKET=openpost-media
OPENPOST_S3_ACCESS_KEY_ID=...
OPENPOST_S3_SECRET_ACCESS_KEY=...
OPENPOST_S3_PUBLIC_BASE_URL=https://media.openpost.example
OPENPOST_S3_FORCE_PATH_STYLE=falseCloud mode
When OPENPOST_EDITION=cloud, OpenPost refuses to start unless:
OPENPOST_STORAGE_DRIVER=s3OPENPOST_S3_REGIONis setOPENPOST_S3_BUCKETis setOPENPOST_S3_ACCESS_KEY_IDis setOPENPOST_S3_SECRET_ACCESS_KEYis setOPENPOST_S3_PUBLIC_BASE_URLis set
OPENPOST_S3_PUBLIC_BASE_URL is required in cloud mode because social networks need stable public media links.
The browser can upload straight to S3 or R2. OpenPost sends larger files in parts without loading the whole file into memory.
For direct browser uploads, the bucket must allow CORS requests from the OpenPost app origin. For Cloudflare R2, apply a bucket CORS rule like this, replacing the origin with your OPENPOST_APP_URL:
{
"rules": [
{
"allowed": {
"origins": ["https://app.openpost.example"],
"methods": ["PUT"],
"headers": ["Content-Type"]
},
"exposeHeaders": ["ETag"],
"maxAgeSeconds": 3600
}
]
}Save the rule as cors.json, apply it with wrangler r2 bucket cors set <bucket> --file cors.json, and verify it with wrangler r2 bucket cors list <bucket>. Without this rule, the browser blocks the upload and reports Failed to fetch.
Streaming upload flow:
- Call
POST /api/v1/media/upload-sessionwithworkspace_id,filename,mime_type, andsize. - Upload the file to the returned
PUTtarget with the returned headers. For a relative OpenPost target, use the same OpenPost bearer credential or browser session. For an absolute presigned bucket target, send only the returned upload headers; never send the OpenPost bearer token or session cookie to the storage host. - Call
POST /api/v1/media/upload-session/{media_id}/completewith the sameworkspace_id.
S3-compatible storage returns a presigned browser-to-bucket target for files within the provider's single-request limit. Larger files use an authenticated OpenPost target and are written to the bucket as 8 MiB multipart parts. Local storage uses the same authenticated streaming target and writes directly to disk. Configure the reverse proxy in front of OpenPost to accept the largest video size you intend to support; X subscribed accounts can upload videos as large as 16 GiB.
OpenPost makes a pending Media item first. After upload, it checks the saved file, finds matching files with SHA-256, makes a thumbnail when possible, records usage, and marks the file ready. It does not keep a large file in app memory.
The web app uses upload sessions automatically for current local and S3-compatible deployments. It falls back to the legacy multipart endpoint only when the server does not advertise upload-session support.
OpenPost can reuse a file it already sent to a social network when one account needs a retry. Networks that fetch a public media link, such as Threads, Instagram, Facebook, and TikTok, get a fresh link instead.