Skip to content

Image Storage

Alexander Elchlepp edited this page Jun 25, 2026 · 1 revision

Image Storage

FewohBee stores user-uploaded images (room category photos, template images) through a pluggable storage layer. Two backends are supported:

  • local (default) — files are written to the local public/resources/images/ tree and served by nginx as static assets. Best for single-server installations.
  • s3 — files are stored in any S3-compatible bucket (Hetzner Object Storage, MinIO, Cloudflare R2, Wasabi, AWS S3, …). Required for Kubernetes / multi-pod deployments where pods must share uploaded content.

The backend is selected via the STORAGE_ADAPTER environment variable and is baked into the compiled Symfony container — changing it requires clearing the cache (bin/console cache:clear).

Existing installations do not need to change anything. STORAGE_ADAPTER defaults to local, the local adapter targets the same public/resources/images/... paths that were used before this setting existed, and generated URLs are identical to the previous behaviour. No file migration is required.


Local storage (default)

No configuration needed. Make sure the web user (www-data) has write permissions on:

  • public/resources/images/export/ — used by the template editor image upload
  • public/resources/images/room-categories/ — used for room category photos (thumb, medium, original variants)

In .env:

STORAGE_ADAPTER=local

S3-compatible storage

Use this for Kubernetes deployments, multi-server installations, or whenever you want to offload images to object storage. The implementation uses the AWS S3 v3 driver, which speaks the standard S3 protocol — it works with any S3-compatible provider, not only AWS.

Bucket setup (example: Hetzner Object Storage)

  1. Create a bucket in your Hetzner Cloud project (Object Storage → Buckets). Choose a location (fsn1, nbg1, hel1).

  2. Generate S3 credentials (Access Key + Secret Key) in the Hetzner Cloud Console.

  3. Set the bucket's permissions to public read so that browsers can load image URLs directly without signed requests.

  4. Configure CORS on the bucket if the booking page is served from a different origin than the bucket URL. A minimal policy that allows GET from any origin:

    <CORSConfiguration>
      <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
      </CORSRule>
    </CORSConfiguration>

.env configuration

STORAGE_ADAPTER=s3
STORAGE_S3_ENDPOINT=https://fsn1.your-objectstorage.com
STORAGE_S3_REGION=fsn1
STORAGE_S3_BUCKET=fewohbee-images
STORAGE_S3_KEY=your-access-key
STORAGE_S3_SECRET=your-secret-key
STORAGE_S3_PUBLIC_URL=https://fewohbee-images.fsn1.your-objectstorage.com
  • STORAGE_S3_ENDPOINT — the provider's regional S3 endpoint. For Hetzner it is https://<region>.your-objectstorage.com.
  • STORAGE_S3_REGION — provider region identifier (Hetzner: fsn1, nbg1, or hel1; MinIO/R2: any value, often us-east-1).
  • STORAGE_S3_BUCKET — bucket name.
  • STORAGE_S3_KEY / STORAGE_S3_SECRET — access credentials.
  • STORAGE_S3_PUBLIC_URL — the public base URL of the bucket. This is the prefix used when building image <img src="..."> URLs for the browser. For Hetzner this is typically https://<bucket>.<region>.your-objectstorage.com.

After updating .env, clear the Symfony cache (only required in prod mode; dev mode picks the change up on the next request):

bin/console cache:clear

Docker setup users: when changing storage settings on a running container, restart the php service so the new environment is applied: docker compose restart php


Migrating existing images to S3

When switching an existing installation from local to s3, copy the contents of the local image directories into the bucket. FewohBee ships a console command for this:

bin/console app:storage:migrate-images

The command iterates public/resources/images/export/ and public/resources/images/room-categories/ and uploads every file into the currently configured storages (STORAGE_ADAPTER=s3 must already be active). Files that already exist at the destination are skipped, so the command is idempotent and safe to re-run.

Options

  • --dry-run — list what would be copied without writing anything. Useful as a sanity check.
  • --overwrite — overwrite files that already exist at the destination. Use this only if you want to refresh the bucket from local files.

Recommended migration workflow

  1. Set up the bucket and credentials as described above.

  2. Edit .env: set STORAGE_ADAPTER=s3 plus all STORAGE_S3_* values.

  3. Clear the cache: bin/console cache:clear.

  4. Verify the configuration with a dry run:

    bin/console app:storage:migrate-images --dry-run
  5. Run the actual migration:

    bin/console app:storage:migrate-images
  6. Open the public booking page and a room category in the settings to confirm that images are now served from the bucket URL (visible in the browser's network tab).

  7. Once you have verified everything works, the local public/resources/images/ directories can be backed up and removed — they are no longer used.

The local directories remain on disk until you delete them, so a rollback to STORAGE_ADAPTER=local is always possible by reverting the .env change.


How image URLs are generated

  • Local mode — URLs are root-relative paths like /resources/images/room-categories/42/medium_photo.jpg. The application's base path is prepended automatically, so the same URL works for installations running at / or in a sub-directory (e.g. /fewohbee).
  • S3 mode — URLs are absolute, pointing directly at the bucket: https://fewohbee-images.fsn1.your-objectstorage.com/room-categories/42/medium_photo.jpg. Browsers load these directly from object storage, no application traffic is involved.

All URL generation goes through a central ImageUrlGenerator service, so both the backend (room category settings, template editor) and the public booking page produce consistent URLs without any caller-side string concatenation.

Clone this wiki locally