You deployed Strapi to production, and a month later files are lost during deployment? Or the backend crashes under load when serving static assets? A familiar situation: the standard Strapi media library stores everything in public/uploads — fine for development, but critical for production. In this article, we'll walk through configuring Strapi storage for production. Over 10 years, we have configured media libraries for 50+ Strapi projects and know how to avoid these issues. We integrate cloud storage based on S3, Cloudinary, or R2, add automatic image optimization, and file type control. We'll cover a proper configuration that eliminates headaches.
Local storage does not solve three key problems. First, files are not replicated across instances: with horizontal scaling, each server sees only its own files. Second, local files are lost when containers are rebuilt (Docker recreates the layer). And finally, performance: serving static files through Node.js is slower than through a cloud provider's CDN. A correct media library configuration reduces page load time by 30–40% and eliminates backup risks. In one month, local storage can lead to significant losses due to downtime, and image optimization saved one client a substantial amount on traffic.
Why You Shouldn't Use Local Storage in Production
Local storage in public/uploads is only suitable for development. In production, it creates bottlenecks: no fault tolerance, difficult scaling, and files can be lost with every deployment. Cloud storage solves all these problems: files are stored centrally, accessible from anywhere via CDN, and are not lost when containers restart.
Which Provider to Choose for the Strapi Media Library?
| Provider | Package | Features |
|---|---|---|
| AWS S3 | @strapi/provider-upload-aws-s3 |
Flexible configuration, CloudFront integration, mature SDK |
| Cloudinary | @strapi/provider-upload-cloudinary |
Built-in image optimization, automatic formats (WebP, AVIF) |
| DigitalOcean Spaces | via aws-s3 |
S3-compatible, fixed price for 250 GB |
| Cloudflare R2 | via aws-s3 |
Zero egress fees, global delivery network |
Cloudinary processes images 2-3 times faster than S3 with Lambda functions. R2 wins on cost with high traffic volumes — no outbound bandwidth charges.
Configuring AWS S3
Install the package: npm install @strapi/provider-upload-aws-s3. Configuration in config/plugins.js:
module.exports = ({ env }) => ({ upload: { config: { provider: 'aws-s3', providerOptions: { s3Options: { credentials: { accessKeyId: env('AWS_ACCESS_KEY_ID'), secretAccessKey: env('AWS_ACCESS_KEY_SECRET'), }, region: env('AWS_REGION', 'eu-central-1'), endpoint: env('AWS_ENDPOINT', undefined), // for S3-compatible params: { ACL: env('AWS_ACL', 'public-read'), Bucket: env('AWS_BUCKET'), }, }, }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, }); Amazon S3 is an object storage service from AWS, widely used for file storage.
Configuring Cloudflare R2
R2 is S3 API-compatible, with free egress:
module.exports = ({ env }) => ({ upload: { config: { provider: 'aws-s3', providerOptions: { s3Options: { credentials: { accessKeyId: env('R2_ACCESS_KEY_ID'), secretAccessKey: env('R2_SECRET_ACCESS_KEY'), }, region: 'auto', endpoint: `https://${env('CF_ACCOUNT_ID')}.r2.cloudflarestorage.com`, params: { Bucket: env('R2_BUCKET'), }, }, }, }, }, }); For public file access, you need a custom R2 domain or a Cloudflare Worker.
Configuring Cloudinary
Install the package: npm install @strapi/provider-upload-cloudinary. Configuration:
module.exports = ({ env }) => ({ upload: { config: { provider: 'cloudinary', providerOptions: { cloud_name: env('CLOUDINARY_NAME'), api_key: env('CLOUDINARY_KEY'), api_secret: env('CLOUDINARY_SECRET'), }, actionOptions: { upload: { folder: env('CLOUDINARY_FOLDER', 'strapi'), transformation: [{ quality: 'auto', fetch_format: 'auto' }], }, uploadStream: {}, delete: {}, }, }, }, }); In one project with 500,000 images, we migrated the media library to Cloudinary in 2 days, reducing page load time from 4.2s to 1.1s. This was made possible by automatic optimization and CDN.
Image Formats and Breakpoints
Strapi automatically creates multiple sizes on upload. Configuration in config/plugins.js:
breakpoints: { xlarge: 1920, large: 1000, medium: 750, small: 500, xsmall: 64, }, In the API response, the formats field contains URLs for each variant. Sizes and typical usage:
| Size | Width (px) | Typical Use |
|---|---|---|
| xlarge | 1920 | Desktop banners |
| large | 1000 | Articles, desktop content |
| medium | 750 | Card images |
| small | 500 | List thumbnails |
| xsmall | 64 | Icons, avatars |
File Type Filtering and Size Limits
Limiting upload size and MIME types:
// config/plugins.js module.exports = { upload: { config: { sizeLimit: 20 * 1024 * 1024, // 20 MB }, }, }; // src/extensions/upload/strapi-server.js module.exports = (plugin) => { const originalUpload = plugin.controllers.upload.upload; plugin.controllers.upload.upload = async (ctx) => { const { files } = ctx.request; const allowed = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']; const invalid = Object.values(files).flat() .filter(f => !allowed.includes(f.type)); if (invalid.length) { return ctx.badRequest('Unsupported file type'); } return originalUpload(ctx); }; return plugin; }; Configuration Process
- Requirements analysis: determine file volume, budget, performance needs.
- Provider selection: based on cost, user geography, transformation needs.
- Provider configuration: install package, set environment variables, test connection.
- Breakpoint configuration: adjust sizes to match site design (typically 640, 1024, 1920).
- Filtering and limits: configure allowed MIME types and maximum file size.
- Data migration: transfer existing files from local disk and update database references.
- Testing: verify upload, display, and performance.
- Documentation and training: hand over instructions to the client's team.
Migration Details
Migration is performed using a script that copies files from `public/uploads` to the S3 bucket and updates records in the Strapi database. For a few gigabytes, the process takes 1-2 days. Before migration, a backup of the database and files is always created. After the transfer, all media object links point to the cloud storage.Timelines
- Setting up S3/R2/Cloudinary provider — a few hours.
- Migrating existing files from local storage — 1 day (depends on volume).
- Custom type filtering + limits — a few hours.
What's Included
- Provider storage configuration (S3, Cloudinary, R2).
- Breakpoint configuration and image optimization.
- File type and size filtering implementation.
- Migration of existing files to cloud storage.
- Documentation and instructions for your team.
- Guaranteed correct operation and support for one month after setup.
Get a consultation on configuring your Strapi media library — we'll help you choose the optimal solution and implement it without downtime. Contact us to discuss your project. Order an audit of your current configuration — we'll check for bottlenecks.







