Shop It Docs
Developer ResourcesUploads & Storage

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

VariableDefaultDescription
UPLOAD_LOCATIONuploadsLocal directory for Multer temporary files. Relative to cwd or absolute.
UPLOAD_MAX_FILE_SIZE_MB50Max file size for admin upload endpoints (MB).
STORAGE_DRIVERautoDriver selection: local, bucket, or auto.

The mobile endpoint (POST /mobile/uploads) has a hardcoded 5 MB limit regardless of UPLOAD_MAX_FILE_SIZE_MB.


Bucket (S3 / MinIO / RustFS)

VariableRequiredDescription
STORAGE_BUCKET_ENDPOINTWhen using buckete.g. http://localhost:9000
STORAGE_BUCKET_ACCESS_KEYWhen using bucketRead-write access key
STORAGE_BUCKET_SECRET_KEYWhen using bucketRead-write secret key
STORAGE_BUCKET_NAMEWhen using bucketBucket name, e.g. apple
STORAGE_BUCKET_REGIONNoDefaults to us-east-1
STORAGE_BUCKET_PUBLIC_URLNoOverride the public base URL (CDN, reverse proxy)
STORAGE_BUCKET_FORCE_PATH_STYLENoSet true for MinIO / RustFS (required)
STORAGE_BUCKET_READONLY_ACCESS_KEYNoRead-only access key for signed URL generation
STORAGE_BUCKET_READONLY_SECRET_KEYNoRead-only secret key for signed URL generation

STORAGE_BUCKET_FORCE_PATH_STYLE=true is 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=50

Signed-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/apple

Corresponding .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:9000

Production: 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-key

When 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, applyBucketPolicy will warn in the logs but will not crash the application (try/catch in onModuleInit). 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.

SettingDefaultNotes
Quality85Configurable via StorageManagerOptions.imageOptimization.quality
Effort6Sharp encoding effort (0 = fast, 9 = smallest)

Non-image files (video, PDF, CSV) pass through optimisation unchanged regardless of the optimize flag.