Skip to Content
FeaturesStorage Backends (S3)

Storage Backends

PicPeak stores photos, thumbnails, hero images, watermarks, and archive zips on either the local filesystem (default) or any S3-compatible object store (AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces). The choice is made entirely with environment variables; no code change is required to switch.

When to use which

Local filesystemS3-compatible
Setup complexityLowestMedium
Bandwidth costFree (server traffic only)Egress charges per GB
Disk pressure on serverHigh — all originals + zips on diskLow — only temp files
Backup storyTar/rsync the storage dirS3 lifecycle policies
Auto-import via folder watcher✅ chokidar✅ via S3 prefix walker (opt-in)
Bulk Download AllStreamed through backendSame

Default to local until you hit one of: low disk on the server, multiple PicPeak instances behind a load balancer, or a desire to push storage cost off the application server.

Switching to S3-compatible storage

1. Provision a bucket

The minimum IAM policy for AWS S3:

{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetBucketLocation" ], "Resource": [ "arn:aws:s3:::picpeak", "arn:aws:s3:::picpeak/*" ] }] }

For non-AWS providers, create a service account / access key with read+write on the bucket. Examples follow.

2. Configure environment variables

STORAGE_BACKEND=s3 STORAGE_S3_BUCKET=picpeak STORAGE_S3_REGION=us-east-1 STORAGE_S3_ACCESS_KEY=AKIAxxxxxxxxxxxxxxxx STORAGE_S3_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxx # STORAGE_S3_ENDPOINT=... # set for MinIO / R2 / B2 / Spaces # STORAGE_S3_PREFIX=picpeak # share a bucket between multiple deployments # STORAGE_S3_FORCE_PATH_STYLE=true # MinIO needs this; auto-on when endpoint is set # STORAGE_S3_SSL=true

Provider-specific examples

MinIO (self-hosted)

STORAGE_S3_ENDPOINT=http://minio.internal:9000 STORAGE_S3_BUCKET=picpeak STORAGE_S3_ACCESS_KEY=minioadmin STORAGE_S3_SECRET_KEY=minioadmin STORAGE_S3_FORCE_PATH_STYLE=true STORAGE_S3_SSL=false

Cloudflare R2

STORAGE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com STORAGE_S3_BUCKET=picpeak STORAGE_S3_REGION=auto STORAGE_S3_ACCESS_KEY=<access-key-id> STORAGE_S3_SECRET_KEY=<secret-access-key>

Backblaze B2

STORAGE_S3_ENDPOINT=https://s3.us-west-002.backblazeb2.com STORAGE_S3_BUCKET=picpeak STORAGE_S3_REGION=us-west-002 STORAGE_S3_ACCESS_KEY=<keyID> STORAGE_S3_SECRET_KEY=<applicationKey>

3. Migrate existing content

Use the migration script before flipping the env on a deployment with existing photos:

node backend/scripts/migrate-storage.js --dry-run node backend/scripts/migrate-storage.js

The script is idempotent (sha256-based skip on already-uploaded files) and writes a CSV of any failures to /tmp/migrate-storage-failures.csv. It does NOT flip STORAGE_BACKEND — admins do that explicitly after the migration completes clean.

4. Restart

PicPeak’s startup ping HEADs a sentinel key on the bucket and refuses to boot on misconfiguration, so you’ll see clear error messages if anything is wrong.

Presigned-URL “Download All” mode (removed)

Earlier versions had a per-event opt-in, Allow direct S3 download (no watermark, S3 mode only), that redirected Download All to a short-lived presigned S3 URL of the cached zip. It was removed on the beta channel in v3.130.2-beta.0 (PicPeak PR #1357 ) and is not in any stable release from v3.132.1, so that revoking a guest’s access also stops their downloads. Download All is always streamed through the backend, which applies the gallery’s access rules, download limit and watermark.

S3 auto-import (drop-folder workflow)

In local mode the chokidar watcher imports any photo dropped into events/active/<slug>/. S3 has no inotify equivalent, so PicPeak ships an opt-in prefix walker that polls every active event’s S3 prefix on a slow cadence.

Enable with:

STORAGE_AUTO_IMPORT=true STORAGE_AUTO_IMPORT_INTERVAL_MS=300000 # 5 minutes (default)

How it behaves:

  • Every poll, lists each active event’s prefix
  • An object is imported only after it’s been seen for two consecutive polls (eventual-consistency gate — avoids flapping when S3 returns a freshly-uploaded object that disappears on the next list)
  • Generated artifacts (thumbnails, hero images, dot-files) are skipped
  • Imports fire photo.uploaded webhooks the same way the local file watcher does
  • Every photo lands at the gallery root; folders in the prefix are not recreated as gallery folders

Off by default because it adds API call cost; admins who only use the upload UI/API don’t pay it.

Camera RAW and capture dates

On the beta channel from v3.174.0-beta.0 (PicPeak PR #2027 ):

  • The S3 auto-import takes camera RAW files (.cr2, .nef, .arw, …) when their extension is on the Allowed File Types list, and reads thumbnail and dimensions from the embedded preview. Earlier builds skipped every RAW except .dng.
  • Behaviour change for .dng: it is now skipped too unless dng is on the list, as for uploads and the local watcher.
  • Photos from the S3 auto-import, the local watcher and the v1 API now store their capture date, so sorting by date taken works for them.

The local drop-folder watcher (events/active/<slug>/) imports camera RAW the same way on the beta channel from v3.170.0-beta.0 (PicPeak PR #1968 ): a RAW whose extension is on the allowed list is imported with its embedded preview as thumbnail; one that is not is skipped with a warning in the log naming the file. Videos dropped there are now stored as videos. Stable’s watcher takes no RAW: only .jpg, .jpeg, .png, .webp and videos.

Capability matrix

CapabilityLocalS3
Photo / thumbnail / hero serving✅ stream✅ stream
Bulk Download All (cached zip)✅ stream✅ stream
Backups✅✅
File-watcher auto-import✅ chokidar✅ S3 prefix walker (opt-in)
External media reference mode and folder watching (EXTERNAL_MEDIA_ROOT)✅ (always local)✅ (still local — not migrated)
Watermarks✅✅

Reverting from S3 to local

Not supported in v1. The migration script is one-way. If you need to revert, you’d download every key with aws s3 sync and update photos.path rows by hand.

Last updated on