Files
documenso/apps/docs/content/docs/self-hosting/configuration/storage.mdx
T
2026-05-27 11:58:39 +07:00

679 lines
19 KiB
Plaintext

---
title: Storage Configuration
description: Configure file storage for uploaded documents and signed PDFs using database storage (default), S3-compatible object storage, or Azure Blob Storage.
---
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
## Storage Options
| Backend | Best For | Scalability | Configuration |
| ------------ | --------------------------------------- | ----------- | ------------- |
| `database` | Small deployments, simplicity | Limited | None required |
| `s3` | Production, large files, backups | High | Required |
| `azure-blob` | Production on Azure, native Blob access | High | Required |
Select the storage backend with the `NEXT_PUBLIC_UPLOAD_TRANSPORT` environment variable:
```bash
# Database storage (default)
NEXT_PUBLIC_UPLOAD_TRANSPORT=database
# S3-compatible storage
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
# Azure Blob Storage (native)
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
```
---
## Database Storage
Database storage is the default option and requires no additional configuration. Documents are stored as base64-encoded data directly in PostgreSQL.
<Tabs items={['Advantages', 'Limitations']}>
<Tab value="Advantages">
- No external dependencies
- Simple deployment
- Automatic backups with database
</Tab>
<Tab value="Limitations">
- Increases database size significantly
- Slower for large files
- Database backup/restore takes longer
- Not recommended for files larger than 10MB
</Tab>
</Tabs>
### Configuration
No configuration required. Database storage is enabled when `NEXT_PUBLIC_UPLOAD_TRANSPORT` is unset or set to `database`.
---
## S3 Configuration
S3 storage is recommended for production deployments. Documenso supports AWS S3 and any S3-compatible storage service.
### Required Variables
| Variable | Description |
| --------------------------------------- | --------------------------------- |
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Set to `s3` |
| `NEXT_PRIVATE_UPLOAD_BUCKET` | S3 bucket name |
| `NEXT_PRIVATE_UPLOAD_REGION` | AWS region (default: `us-east-1`) |
| `NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID` | AWS access key ID |
| `NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY` | AWS secret access key |
### Optional Variables
| Variable | Description | Default |
| -------------------------------------- | --------------------------------------------- | ----------- |
| `NEXT_PRIVATE_UPLOAD_ENDPOINT` | Custom S3 endpoint for S3-compatible services | |
| `NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE` | Use path-style URLs instead of virtual-hosted | `false` |
| `NEXT_PRIVATE_UPLOAD_REGION` | S3 region | `us-east-1` |
---
## AWS S3 Setup
{/* prettier-ignore */}
<Steps>
<Step>
### Create an S3 Bucket
Create a bucket in the AWS Console or using the CLI:
```bash
aws s3 mb s3://your-documenso-bucket --region us-east-1
```
</Step>
<Step>
### Configure Bucket Policy
Block public access and configure CORS for presigned URL uploads:
**CORS Configuration:**
```json
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedOrigins": ["https://your-documenso-domain.com"],
"ExposeHeaders": ["ETag"]
}
]
```
Apply via AWS Console (Bucket > Permissions > CORS configuration) or CLI:
```bash
aws s3api put-bucket-cors --bucket your-documenso-bucket --cors-configuration file://cors.json
```
</Step>
<Step>
### Create IAM User
Create an IAM user with programmatic access and attach this policy:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::your-documenso-bucket/*"
}
]
}
```
</Step>
<Step>
### Configure Environment Variables
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
NEXT_PRIVATE_UPLOAD_BUCKET=your-documenso-bucket
NEXT_PRIVATE_UPLOAD_REGION=us-east-1
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
```
</Step>
</Steps>
---
## MinIO Setup
MinIO is a self-hosted S3-compatible object storage server.
{/* prettier-ignore */}
<Steps>
<Step>
### Deploy MinIO
Using Docker:
```bash
docker run -d \
--name minio \
-p 9000:9000 \
-p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
-v minio_data:/data \
minio/minio server /data --console-address ":9001"
```
Using Docker Compose with Documenso:
```yaml
services:
minio:
image: minio/minio
command: server /data --console-address ":9001"
ports:
- '9000:9000'
- '9001:9001'
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio_data:/data
volumes:
minio_data:
```
</Step>
<Step>
### Create a Bucket
Access the MinIO Console at `http://localhost:9001` and create a bucket, or use the CLI:
```bash
# Install MinIO client
mc alias set myminio http://localhost:9000 minioadmin minioadmin
# Create bucket
mc mb myminio/documenso
```
</Step>
<Step>
### Configure Environment Variables
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
NEXT_PRIVATE_UPLOAD_BUCKET=documenso
NEXT_PRIVATE_UPLOAD_ENDPOINT=http://minio:9000
NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=true
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=minioadmin
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=minioadmin
NEXT_PRIVATE_UPLOAD_REGION=us-east-1
```
</Step>
</Steps>
<Callout type="info">
Set `NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=true` for MinIO and other S3-compatible services that
don't support virtual-hosted bucket URLs.
</Callout>
---
## Other S3-Compatible Services
Documenso works with any S3-compatible storage service. Configure the endpoint and enable path-style URLs if required.
### Cloudflare R2
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
NEXT_PRIVATE_UPLOAD_BUCKET=documenso
NEXT_PRIVATE_UPLOAD_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-r2-access-key
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-r2-secret-key
NEXT_PRIVATE_UPLOAD_REGION=auto
```
### DigitalOcean Spaces
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
NEXT_PRIVATE_UPLOAD_BUCKET=documenso
NEXT_PRIVATE_UPLOAD_ENDPOINT=https://nyc3.digitaloceanspaces.com
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-spaces-key
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-spaces-secret
NEXT_PRIVATE_UPLOAD_REGION=nyc3
```
### Backblaze B2
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
NEXT_PRIVATE_UPLOAD_BUCKET=documenso
NEXT_PRIVATE_UPLOAD_ENDPOINT=https://s3.us-west-004.backblazeb2.com
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-b2-key-id
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-b2-application-key
NEXT_PRIVATE_UPLOAD_REGION=us-west-004
```
### Wasabi
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
NEXT_PRIVATE_UPLOAD_BUCKET=documenso
NEXT_PRIVATE_UPLOAD_ENDPOINT=https://s3.us-east-1.wasabisys.com
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-wasabi-key
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-wasabi-secret
NEXT_PRIVATE_UPLOAD_REGION=us-east-1
```
---
## Azure Blob Storage
Azure Blob Storage is supported as a native transport (not S3-compatible). Documenso uses the official `@azure/storage-blob` SDK and signs SAS URLs with the Storage Account key for browser uploads and downloads.
### Required Variables
| Variable | Description |
| --------------------------------------- | ------------------------------------------------- |
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Set to `azure-blob` |
| `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME` | Azure Storage Account name |
| `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY` | Azure Storage Account access key |
| `NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER` | Container name where uploads are stored |
### Optional Variables
| Variable | Description | Default |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT` | Custom Blob endpoint URL. Useful for local development against Azurite (for example `http://127.0.0.1:10000`). | `https://<account>.blob.core.windows.net` |
### Azure Setup
{/* prettier-ignore */}
<Steps>
<Step>
### Create a Storage Account and Container
Create a Storage Account in the Azure Portal or via the Azure CLI, then create a container inside it:
```bash
az storage account create \
--name yourstorageaccount \
--resource-group your-rg \
--location eastus \
--sku Standard_LRS
az storage container create \
--name documenso-documents \
--account-name yourstorageaccount
```
</Step>
<Step>
### Configure CORS on the container
The browser uploads documents directly to Azure Blob using a SAS URL, and downloads them the same way, so the Storage Account needs CORS rules that allow your application origin:
```bash
az storage cors add \
--services b \
--methods GET PUT \
--origins https://your-documenso-domain.com \
--allowed-headers "Content-Type" "x-ms-blob-type" "Authorization" \
--exposed-headers "*" \
--max-age 3600 \
--account-name yourstorageaccount
```
</Step>
<Step>
### Configure Environment Variables
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=yourstorageaccount
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=your-account-key
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents
```
</Step>
</Steps>
### Local Development with Azurite
Azurite is the official Azure Storage emulator. It supports the Blob REST API with account-key authentication.
```bash
docker run -d --name azurite \
-p 10000:10000 -p 10001:10001 -p 10002:10002 \
mcr.microsoft.com/azure-storage/azurite
```
Create the container against the well-known development account:
```bash
az storage container create \
--name documenso-documents \
--connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
```
Configure environment variables to point at the emulator:
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=devstoreaccount1
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents
NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT=http://127.0.0.1:10000
```
<Callout type="info">
The Azurite key shown above is the public well-known development key, published by Microsoft for emulator use. Never reuse it in production.
</Callout>
---
## CloudFront CDN (Optional)
Use Amazon CloudFront to serve documents with lower latency and reduced S3 costs. CloudFront integration uses signed URLs for secure access.
### Prerequisites
- An S3 bucket configured for Documenso
- A CloudFront distribution with the S3 bucket as origin
- A CloudFront key pair for signing URLs
{/* prettier-ignore */}
<Steps>
<Step>
### Create a CloudFront Distribution
- Go to CloudFront in the AWS Console
- Create a distribution with your S3 bucket as the origin
- Configure Origin Access Control (OAC) to restrict direct S3 access
- Set the default cache behavior to allow GET requests
</Step>
<Step>
### Create a Key Pair
CloudFront signed URLs require a key pair:
- Go to CloudFront > Key management > Public keys
- Create a new public key
- Create a key group containing the public key
- Associate the key group with your distribution
Keep the private key secure - you'll need it for the environment variable.
</Step>
<Step>
### Configure Environment Variables
```bash
# CloudFront distribution domain (without https://)
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN=d1234567890.cloudfront.net
# CloudFront key pair ID
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID=K1234567890ABC
# Private key contents (PEM format)
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"
```
<Callout type="warn">
Store the private key securely. Use environment variables or secrets management rather than
committing it to version control.
</Callout>
</Step>
</Steps>
### How It Works
When CloudFront is configured:
{/* prettier-ignore */}
<Steps>
<Step>
### Uploads
File uploads still go directly to S3 via presigned URLs.
</Step>
<Step>
### Downloads
File downloads use CloudFront signed URLs.
</Step>
<Step>
### Caching
CloudFront caches files at edge locations.
</Step>
<Step>
### Expiration
Signed URLs expire after 1 hour.
</Step>
</Steps>
---
## Migration Between Storage Backends
Documenso does not provide automatic migration between storage backends. Each document's storage location is recorded in the database.
<Callout type="warn">
Documents uploaded to one storage backend cannot be automatically migrated to another. Plan your
storage strategy before deploying to production.
</Callout>
### Manual Migration Process
To migrate existing documents from database to S3 storage:
{/* prettier-ignore */}
<Steps>
<Step>
### Export documents
Extract document blobs from the database (e.g. via a script querying `DocumentData` where `type` is `BYTES_64`).
</Step>
<Step>
### Upload to S3
Upload each exported file to your S3 bucket and note the resulting object keys or paths.
</Step>
<Step>
### Update DocumentData records
Point each record to the new S3 location by updating `DocumentData` with the S3 path and setting `type` to `S3_PATH`.
</Step>
</Steps>
This requires custom scripts and database modifications.
<Callout type="info">
For production deployments, we recommend starting with S3 storage from the beginning.
</Callout>
### Hybrid Operation
During migration, Documenso can read from both backends. The `DocumentData.type` field indicates where each document is stored.
- `BYTES_64`: Stored in database
- `S3_PATH`: Stored in S3
New uploads use the configured `NEXT_PUBLIC_UPLOAD_TRANSPORT` backend.
---
## Storage Sizing
### Database Storage Estimates
When using database storage, plan for significant database growth:
| Documents/Month | Avg Size | Monthly Growth | Annual Growth |
| --------------- | -------- | -------------- | ------------- |
| 100 | 500KB | ~50MB | ~600MB |
| 1,000 | 500KB | ~500MB | ~6GB |
| 10,000 | 500KB | ~5GB | ~60GB |
Database storage includes base64 encoding overhead (~33% increase).
### S3 Storage Estimates
S3 stores files without encoding overhead:
| Documents/Month | Avg Size | Monthly Growth | Annual Growth |
| --------------- | -------- | -------------- | ------------- |
| 100 | 500KB | ~50MB | ~600MB |
| 1,000 | 500KB | ~500MB | ~6GB |
| 10,000 | 500KB | ~5GB | ~60GB |
### Cost Comparison
For high-volume deployments, S3 is more cost-effective:
| Aspect | Database Storage | S3 Storage |
| ------------- | ---------------------------- | ----------------------- |
| Storage cost | Database pricing (~$0.10/GB) | S3 pricing (~$0.023/GB) |
| Transfer cost | Database I/O | S3 requests + egress |
| Backup cost | Larger database backups | Separate S3 backups |
| Performance | Degrades with size | Consistent |
---
## Upload Size Limits
Configure the maximum upload size displayed to users:
```bash
NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=10
```
This value is in megabytes. The default is 5MB.
<Callout type="info">
This environment variable controls the UI display. Actual limits may also be enforced by your
reverse proxy, web server, or S3 configuration.
</Callout>
Ensure your infrastructure supports the configured limit:
<Tabs items={['Nginx', 'S3', 'CloudFront']}>
<Tab value="Nginx">
Set `client_max_body_size` to match or exceed your upload limit.
</Tab>
<Tab value="S3">
Default object size limit is 5GB; multipart upload may be required for large files.
</Tab>
<Tab value="CloudFront">
Default limit is 50MB per request.
</Tab>
</Tabs>
---
## Troubleshooting
<Accordions type="multiple">
<Accordion title="Access Denied when uploading">
Causes:
- Incorrect IAM credentials
- Bucket policy doesn't allow required operations
- CORS not configured for presigned URL uploads
Solutions:
- Verify IAM user has `s3:PutObject` permission
- Check bucket policy allows writes
- Configure CORS to allow your domain
</Accordion>
<Accordion title="Invalid endpoint (getaddrinfo ENOTFOUND)">
Causes:
- Incorrect region configuration
- Bucket doesn't exist
- Network connectivity issues
Solutions:
- Verify the bucket exists in the specified region
- Check the endpoint URL is correct
- Verify network access to S3
</Accordion>
<Accordion title="SignatureDoesNotMatch">
Causes:
- Incorrect secret access key
- Clock skew between server and S3
Solutions:
- Verify credentials are correct and not expired
- Ensure server time is synchronized (use NTP)
</Accordion>
<Accordion title="Path-style access required">
Set path-style access for S3-compatible services:
```bash
NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=true
```
</Accordion>
<Accordion title="CloudFront signed URL errors (Missing required key 'Key')">
Causes:
- Key pair not configured correctly
- Private key format invalid
Solutions:
- Verify the key pair ID matches the CloudFront configuration
- Ensure the private key is in PEM format
- Check for whitespace or encoding issues in the key contents
</Accordion>
</Accordions>
---
## See Also
- [Database Configuration](/docs/self-hosting/configuration/database) - Configure PostgreSQL
- [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference
- [Backups](/docs/self-hosting/maintenance/backups) - Backup strategies for both storage backends
- [Docker Compose](/docs/self-hosting/deployment/docker-compose) - Deploy with MinIO for local storage