Storage Configuration
Postiz supports two storage backends for media uploads: local filesystem storage and Cloudflare R2 (S3-compatible object storage).
Overview
Choose the storage provider that best fits your deployment:
| Feature | Local Storage | Cloudflare R2 |
|---|
| Cost | Free | Pay per usage |
| Scalability | Limited | Unlimited |
| Backup | Manual | Automatic |
| CDN | Manual setup | Built-in |
| Best for | Development, small deployments | Production, large scale |
Cloudflare R2 is required for production deployments to properly save social media avatars and profile images.
Local Storage
Local storage saves uploaded files directly to the server’s filesystem.
Configuration
STORAGE_PROVIDER="local"
UPLOAD_DIRECTORY="/uploads"
NEXT_PUBLIC_UPLOAD_DIRECTORY="/uploads"
Docker Compose Setup
The Docker Compose configuration includes a volume for local uploads:
postiz:
volumes:
- postiz-uploads:/uploads/
environment:
STORAGE_PROVIDER: 'local'
UPLOAD_DIRECTORY: '/uploads'
NEXT_PUBLIC_UPLOAD_DIRECTORY: '/uploads'
volumes:
postiz-uploads:
external: false
Accessing Uploaded Files
Files are served directly from the Postiz container:
http://localhost:4007/uploads/filename.jpg
Backup Local Uploads
Regularly backup your uploads directory:
# Create backup archive
docker run --rm \
-v postiz_postiz-uploads:/uploads \
-v $(pwd):/backup \
alpine tar czf /backup/uploads-$(date +%Y%m%d).tar.gz -C /uploads .
# Restore from backup
docker run --rm \
-v postiz_postiz-uploads:/uploads \
-v $(pwd):/backup \
alpine tar xzf /backup/uploads-20260303.tar.gz -C /uploads
Local Storage Limitations
Local storage has several limitations:
- Single server: Files are tied to one container
- No CDN: No built-in content delivery network
- Manual backups: You must implement your own backup strategy
- Limited scalability: Disk space is finite
- No redundancy: Single point of failure
For production deployments, use Cloudflare R2.
Cloudflare R2 Storage
Cloudflare R2 provides S3-compatible object storage with zero egress fees.
Why Cloudflare R2?
Benefits of Cloudflare R2:
- Zero egress fees: No charges for data transfer out
- S3 compatible: Works with existing S3 tools
- Global CDN: Automatically distributed worldwide
- Automatic backups: Built-in redundancy
- Unlimited storage: Scale infinitely
- Required for avatars: Needed for social media profile images
Prerequisites
Create Cloudflare Account
Enable R2
- Go to R2 in your Cloudflare dashboard
- Enable R2 (requires payment method, but has free tier)
Create R2 Bucket
- Click “Create bucket”
- Name it (e.g.,
postiz-uploads)
- Choose location (closest to your users)
Generate API Token
- Go to R2 > Manage R2 API Tokens
- Create API token
- Set permissions:
Object Read & Write
- Save Access Key ID and Secret Access Key
Configuration
# Set storage provider to Cloudflare
STORAGE_PROVIDER="cloudflare"
# Cloudflare Account ID (from R2 dashboard)
CLOUDFLARE_ACCOUNT_ID="your-account-id"
# R2 API Credentials
CLOUDFLARE_ACCESS_KEY="your-access-key-id"
CLOUDFLARE_SECRET_ACCESS_KEY="your-secret-access-key"
# Bucket Configuration
CLOUDFLARE_BUCKETNAME="postiz-uploads"
CLOUDFLARE_BUCKET_URL="https://your-bucket-url.r2.cloudflarestorage.com/"
# Region (usually 'auto')
CLOUDFLARE_REGION="auto"
Finding Your Configuration Values
Account ID
- Go to Cloudflare Dashboard
- Select R2
- Your Account ID is displayed on the right sidebar
Account ID: 1a2b3c4d5e6f7g8h9i0j
Access Keys
- Go to R2 > Manage R2 API Tokens
- Create API token
- Copy the Access Key ID and Secret Access Key
Access Key ID: 9a8b7c6d5e4f3g2h1i0j
Secret Access Key: k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6
Save the Secret Access Key immediately - you won’t be able to see it again!
Bucket Name
The name you chose when creating the bucket:
Bucket URL
- Go to your bucket in R2
- Click on Settings
- Find the “Public R2.dev subdomain” or custom domain
https://pub-1a2b3c4d5e6f7g8h.r2.dev/
Or use your custom domain:
https://cdn.yourdomain.com/
Docker Compose Configuration
postiz:
environment:
# Storage Settings
STORAGE_PROVIDER: 'cloudflare'
# Cloudflare R2 Settings
CLOUDFLARE_ACCOUNT_ID: 'your-account-id'
CLOUDFLARE_ACCESS_KEY: 'your-access-key'
CLOUDFLARE_SECRET_ACCESS_KEY: 'your-secret-access-key'
CLOUDFLARE_BUCKETNAME: 'postiz-uploads'
CLOUDFLARE_BUCKET_URL: 'https://your-bucket-url.r2.cloudflarestorage.com/'
CLOUDFLARE_REGION: 'auto'
Bucket Permissions
Ensure your API token has the correct permissions:
{
"effect": "allow",
"actions": [
"storage:object:read",
"storage:object:write",
"storage:object:delete"
],
"resources": [
"bucket:postiz-uploads"
]
}
Public Access Configuration
Enable Public Access
- Go to your R2 bucket settings
- Enable “Public Access”
- Choose “Allow” for R2.dev subdomain
Configure CORS (Optional)
If you need direct browser uploads:[
{
"AllowedOrigins": ["https://postiz.yourdomain.com"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
Custom Domain for R2
Use your own domain instead of R2.dev subdomain:
Add Custom Domain
- Go to your R2 bucket
- Click “Settings” > “Custom Domains”
- Click “Connect Domain”
- Enter your domain (e.g.,
cdn.yourdomain.com)
Verify Domain
Cloudflare will automatically add DNS records if your domain is on Cloudflare.
Update Configuration
CLOUDFLARE_BUCKET_URL="https://cdn.yourdomain.com/"
R2 Pricing
Cloudflare R2 offers generous free tier:
| Resource | Free Tier | Overage Cost |
|---|
| Storage | 10 GB | $0.015/GB/month |
| Class A Operations* | 1M/month | $4.50/million |
| Class B Operations** | 10M/month | $0.36/million |
| Egress | Unlimited | $0 |
*Class A: Writes, Lists, Deletes
**Class B: Reads
R2’s zero egress fees make it significantly cheaper than AWS S3 or other providers for high-traffic sites.
Switching Storage Providers
From Local to R2
Set Up R2 Bucket
Follow the R2 configuration steps above.
Migrate Existing Files
Use rclone to copy files from local storage to R2:# Install rclone
curl https://rclone.org/install.sh | sudo bash
# Configure R2 remote
rclone config
# Choose: s3 compatible
# Provider: Cloudflare
# Enter your credentials
# Copy files
rclone copy /path/to/uploads r2:postiz-uploads
Update Environment Variables
STORAGE_PROVIDER="cloudflare"
# Add R2 credentials...
Restart Postiz
docker compose down
docker compose up -d
From R2 to Local
Moving from R2 to local storage is not recommended for production deployments.
# Download all files from R2
rclone copy r2:postiz-uploads /path/to/local/uploads
# Update environment
STORAGE_PROVIDER="local"
UPLOAD_DIRECTORY="/uploads"
# Restart Postiz
docker compose down
docker compose up -d
Storage Testing
Test Upload Functionality
Access Postiz
Log in to your Postiz instance
Upload Test Image
- Create a new post
- Upload an image
- Verify the image appears correctly
Check Storage Location
Local Storage:docker compose exec postiz ls -lh /uploads
R2 Storage:
Check your R2 bucket in the Cloudflare dashboard
Verify File URLs
Test that uploaded files are accessible:
# Local storage
curl -I http://localhost:4007/uploads/test-image.jpg
# R2 storage
curl -I https://your-bucket-url.r2.cloudflarestorage.com/test-image.jpg
Expected response:
HTTP/2 200
content-type: image/jpeg
content-length: 12345
Troubleshooting
Local Storage Issues
Permission Denied
# Fix volume permissions
docker compose exec postiz chown -R node:node /uploads
docker compose exec postiz chmod -R 755 /uploads
Disk Space Full
# Check volume usage
docker system df -v
# Clean up old files
docker compose exec postiz find /uploads -type f -mtime +90 -delete
R2 Storage Issues
Access Denied
Common causes:
- Invalid credentials: Double-check Access Key and Secret
- Insufficient permissions: Ensure API token has read/write access
- Wrong bucket name: Verify bucket name matches exactly
- IP allowlist: Check if your server’s IP is allowed
# Test R2 connection
docker compose exec postiz node -e '
const { S3Client, ListBucketsCommand } = require("@aws-sdk/client-s3");
const client = new S3Client({
region: "auto",
endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY,
secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY,
},
});
client.send(new ListBucketsCommand({})).then(console.log).catch(console.error);
'
Upload Fails
# Check Postiz logs
docker compose logs postiz | grep -i "upload\|storage\|r2"
Wrong Bucket URL
Ensure the bucket URL ends with a /:
# Correct
CLOUDFLARE_BUCKET_URL="https://pub-abc123.r2.dev/"
# Incorrect
CLOUDFLARE_BUCKET_URL="https://pub-abc123.r2.dev"
Advanced Configuration
Content Delivery Network (CDN)
For R2, Cloudflare CDN is built-in. For local storage, use nginx:
location /uploads/ {
alias /var/www/postiz/uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
Image Optimization
Enable Cloudflare Image Resizing for R2:
- Enable Cloudflare Images
- Configure image transformations
- Use resized URLs:
https://your-bucket-url.r2.dev/cdn-cgi/image/width=800/image.jpg
Storage Lifecycle Policies
Automate cleanup of old files in R2:
- Go to R2 bucket settings
- Create lifecycle rule
- Delete objects older than X days
{
"id": "delete-old-uploads",
"status": "Enabled",
"filter": {
"prefix": "temp/"
},
"expiration": {
"days": 30
}
}
Best Practices
Storage Best Practices:
- Use R2 for production - More reliable and scalable
- Enable versioning - Protect against accidental deletions
- Set up backups - Even with R2, maintain external backups
- Monitor usage - Track storage costs and usage patterns
- Implement cleanup - Remove old/unused files regularly
- Use CDN - Serve files through a CDN for better performance
- Secure access - Use signed URLs for private files
- Test recovery - Regularly test your backup restoration process
Next Steps
Review Environment Variables
Start Using Postiz
Your storage is configured! Start uploading media and scheduling posts.