# 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](https://console.hetzner.cloud/) 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 `
` 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`:
```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.
## 3. Configuration
```bash
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://.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) |
The default admin login is `admin` / `changeme` — **change 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
```bash
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.