Configuration
Environment variables, local development setup, and MinIO/RustFS production configuration.
Configuration
Audience: backend, devops Scope:
apps/api/src/config/env.validation.ts,apps/api/sample.env
Environment Variables
Core Upload Settings
| Variable | Default | Description |
|---|---|---|
UPLOAD_LOCATION | uploads | Local directory for Multer temporary files. Relative to cwd or absolute. |
UPLOAD_MAX_FILE_SIZE_MB | 50 | Max file size for admin upload endpoints (MB). |
STORAGE_DRIVER | auto | Driver selection: local, bucket, or auto. |
The mobile endpoint (
POST /mobile/uploads) has a hardcoded 5 MB limit regardless ofUPLOAD_MAX_FILE_SIZE_MB.
Bucket (S3 / MinIO / RustFS)
| Variable | Required | Description |
|---|---|---|
STORAGE_BUCKET_ENDPOINT | When using bucket | e.g. http://localhost:9000 |
STORAGE_BUCKET_ACCESS_KEY | When using bucket | Read-write access key |
STORAGE_BUCKET_SECRET_KEY | When using bucket | Read-write secret key |
STORAGE_BUCKET_NAME | When using bucket | Bucket name, e.g. apple |
STORAGE_BUCKET_REGION | No | Defaults to us-east-1 |
STORAGE_BUCKET_PUBLIC_URL | No | Override the public base URL (CDN, reverse proxy) |
STORAGE_BUCKET_FORCE_PATH_STYLE | No | Set true for MinIO / RustFS (required) |
STORAGE_BUCKET_READONLY_ACCESS_KEY | No | Read-only access key for signed URL generation |
STORAGE_BUCKET_READONLY_SECRET_KEY | No | Read-only secret key for signed URL generation |
STORAGE_BUCKET_FORCE_PATH_STYLE=trueis required for MinIO and RustFS. AWS S3 uses virtual-hosted style by default (bucket.s3.amazonaws.com), but MinIO uses path style (localhost:9000/bucket).
Local Development Setup
With STORAGE_DRIVER=local (or auto with no bucket credentials), all files are written to UPLOAD_LOCATION on disk and served locally.
# apps/api/.env
UPLOAD_LOCATION=apps/api/uploads
STORAGE_DRIVER=local
UPLOAD_MAX_FILE_SIZE_MB=50Signed-URL responses return a path like /uploads/image/abc.webp with expiresAt: null. You need to serve the uploads directory as static files. In the NestJS main.ts:
app.useStaticAssets(join(process.cwd(), 'apps/api/uploads'), {
prefix: '/uploads',
});The new folder structure also requires /public to be served:
app.useStaticAssets(join(process.cwd(), 'apps/api/uploads/public'), {
prefix: '/public',
});MinIO with Docker Compose
Add MinIO to the root docker-compose.yml for local development with the full bucket experience:
services:
minio:
image: minio/minio:latest
container_name: minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000" # S3 API
- "9001:9001" # MinIO Console (web UI)
volumes:
- minio_data:/data
volumes:
minio_data:Then create the bucket via the MinIO Console at http://localhost:9001 or with the CLI:
# Install mc (MinIO client)
brew install minio/stable/mc # macOS
mc alias set local http://localhost:9000 minioadmin minioadmin
mc mb local/appleCorresponding .env settings:
STORAGE_DRIVER=bucket
STORAGE_BUCKET_ENDPOINT=http://localhost:9000
STORAGE_BUCKET_ACCESS_KEY=minioadmin
STORAGE_BUCKET_SECRET_KEY=minioadmin
STORAGE_BUCKET_NAME=apple
STORAGE_BUCKET_REGION=us-east-1
STORAGE_BUCKET_FORCE_PATH_STYLE=true
STORAGE_BUCKET_PUBLIC_URL=http://localhost:9000Production: Read-only Credential Separation
Create two MinIO service accounts with separate policies:
Policy: storage-readwrite (for uploads and deletes)
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::apple/*"]
}]
}Policy: storage-readonly (for signed URL generation only)
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::apple/*"]
}]
}Then configure:
# Primary credentials — used for uploads and deletes
STORAGE_BUCKET_ACCESS_KEY=rw-access-key
STORAGE_BUCKET_SECRET_KEY=rw-secret-key
# Read-only credentials — used for signed URL generation
STORAGE_BUCKET_READONLY_ACCESS_KEY=ro-access-key
STORAGE_BUCKET_READONLY_SECRET_KEY=ro-secret-keyWhen readonly credentials are set, the app registers a separate SIGNED_URL_STORAGE_MANAGER with those credentials. Even if a signed URL or its generation key leaks, it can only be used to read — not to write or delete files.
Bucket Policy on Startup
When using the bucket driver, UploadService.onModuleInit() automatically applies the bucket policy that makes public/* anonymously readable:
This is idempotent — re-applying the same policy on every restart is safe and has no side effects.
If the bucket does not exist at startup,
applyBucketPolicywill warn in the logs but will not crash the application (try/catchinonModuleInit). Create the bucket before deploying to production.
Image Optimisation
When ?optimize=true (the default), uploaded images are converted to WebP using Sharp before being sent to storage.
| Setting | Default | Notes |
|---|---|---|
| Quality | 85 | Configurable via StorageManagerOptions.imageOptimization.quality |
| Effort | 6 | Sharp encoding effort (0 = fast, 9 = smallest) |
Non-image files (video, PDF, CSV) pass through optimisation unchanged regardless of the optimize flag.