SETUP.md 6.6 KB

Setup & Deployment

1. Requirements

  • Shared webhosting with PHP 8.1 or newer, curl and openssl extensions (both are standard), and Apache .htaccess support.
  • A Hetzner Object Storage bucket (any S3-compatible storage works).
  • No database, no Composer, no build step.

2. Hetzner Object Storage

  1. In the Hetzner Cloud Console create a Bucket (e.g. location fsn1). Set visibility to private — visitors get access only through short-lived presigned URLs.
  2. Create S3 credentials (Security → S3 credentials) and note the access key and secret key.

No bucket CORS rule is needed: uploads are proxied through the webhost (same-origin) and gallery images load via <img> presigned GET URLs, which browsers don't subject to CORS. Keep the bucket private.

2a. PHP upload limits

Uploads stream through PHP one image per request, so the total gallery size is irrelevant — but each single image must fit the host's limits. Ensure upload_max_filesize and post_max_size are at least as large as your biggest original (e.g. 200M for RAW files); on shared hosting set these in .user.ini or php.ini:

upload_max_filesize = 200M
post_max_size       = 200M

max_execution_time is lifted per upload request in code, but if your host caps it at the web-server level (e.g. Apache/FPM request timeout), raise that too for large files.

The browser uploads several images at once (uploads.concurrency, default 3), which keeps the uplink busy while the webhost forwards earlier files to S3. Each one occupies a PHP worker for its whole S3 round trip, so on shared hosting with a tight per-site process limit, lower it:

'concurrency' => 2,   // or 1 to restore strictly serial uploads

If uploads start failing with 503s under load, that limit is the first thing to check.

2b. Download-all archives

Enabling downloads on a gallery builds one ZIP of it into S3, so visitors get a single file without the webhost ever serving the bytes. Two things to know:

  • Storage. The archive roughly doubles that gallery's S3 storage for as long as it exists. Disabling downloads deletes it again.
  • Build traffic. Building copies every photo S3 → webhost → S3 (about twice the gallery's size in webhost traffic), once per rebuild. It is split into slices of archive.step_seconds so no request approaches max_execution_time — the 60 s cap common on shared hosting is fine and needs no change.

Rebuilds normally happen on their own: a page view dispatches worker.php, which runs a slice and then dispatches its successor until the archive is finished. If your host blocks outbound HTTP to itself, that chain cannot start, and archives instead advance one slice per page view. A real cron job removes the guesswork entirely — every 5 minutes is plenty:

*/5 * * * * curl -s "https://www.example.com/worker.php?key=YOUR_WORKER_KEY" >/dev/null

The key is generated on first use and stored in data/worker-key.json; read it from there. Without a valid key the script returns a bare 404.

An interrupted build always resumes where it stopped, and one abandoned for archive.abandon_hours (default 24) is aborted and restarted — which also releases the incomplete multipart upload S3 would otherwise keep billing for.

3. Configuration

cp config/config.sample.php config/config.php
cp config/credentials.sample.php config/credentials.php

Edit config/config.php:

Key Meaning
site.name Fallback site title
site.base_url Public base URL, used for gallery share links
site.timezone Timezone for expiry checks, e.g. Europe/Berlin
s3.endpoint https://<location>.your-objectstorage.com
s3.region The location, e.g. fsn1
s3.bucket Bucket name
s3.access_key / s3.secret_key S3 credentials
s3.url_ttl Lifetime of presigned view URLs in seconds
uploads.thumb_size Longest edge of grid thumbnails (browser-generated)
uploads.resize_quality JPEG quality (0.0–1.0) for galleries that cap their upload resolution

The default admin login is admin / changemechange it in the admin Settings page immediately after the first login.

4. Uploading to the webhost

Upload the contents of this folder into your account's document root (usually public_html/, htdocs/ or www/) via FTP/SFTP. The site's home page, index.php, sits directly in the document root — there is no separate web-root subfolder to configure.

The application internals (app/, config/, data/, docs/) live inside the document root but are blocked from the web by the root .htaccess (plus a deny-all .htaccess inside each of app/, config/, data/ as a fallback).

Verify after deploying — each of these must return 403 Forbidden, never their contents:

  • https://your-domain.com/config/config.php
  • https://your-domain.com/config/credentials.php
  • https://your-domain.com/data/site.json

If they don't, your host ignores .htaccess — move app/, config/ and data/ above the document root and adjust the paths, or contact support.

Writable directories

The PHP process must be able to write to:

  • data/ (and data/galleries/) — flat-file content
  • media/ — hero + showreel images
  • config/ — only for the online password change

On typical shared hosting (suEXEC/FPM running as your user) this already works; otherwise chmod 755 the directories (or 775/777 as a last resort).

5. First-deploy smoke test

  1. Open /admin/, log in, change the password (Settings).
  2. Front page: set your name and intro text, upload a hero image → check the landing page.
  3. Showreel: upload 2–3 images → check /showreel.php, scroll behavior, and that navigation hides when scrolling down.
  4. Galleries: create a test gallery with password and an expiry date of today, upload a handful of images. Then:
    • the upload list shows done for each file (webhost → S3 working),
    • the gallery page asks for the password and then shows images (presigned GETs working),
    • images load from your-objectstorage.com, not from your domain,
    • tomorrow the gallery shows "not available" (expiry working).
  5. Delete the test gallery — the S3 objects are removed as well.

6. Local development

php -S localhost:8080 router.php

router.php reproduces the .htaccess protection for the PHP built-in server (which does not read .htaccess); it is only used locally.

Everything except real S3 traffic works without credentials; gallery pages render presigned URLs that simply won't resolve until real keys are configured.