# Install Photarium

Photarium is a self-hosted photo gallery. This guide installs it on a Linux
server with one command, and covers storage setup, Cloudflare, upgrades,
uninstalling, Windows and troubleshooting.

It is written to be followed top to bottom, by a person or by an AI agent.
Do the steps in order: set up storage (step 3) before running the installer
(step 4).

- Web version: https://photarium.waxquixotic.com/install
- This file: https://photarium.waxquixotic.com/install.md
- Downloads: https://photarium.waxquixotic.com/download

Contents:

1. What you need
2. Choose how HTTPS works
3. Set up storage (Backblaze B2, Wasabi, Cloudflare R2, other S3-compatible)
4. Install
5. Updates
6. Uninstall
7. Cloudflare recommendations
8. Windows (edge cases)
9. Troubleshooting

---

## 1. What you need

- **A Linux server** — Debian 12+, Ubuntu 22.04+, or Fedora 39+; amd64 or
  arm64 — with systemd and root or sudo access.
- **A domain name** for the gallery, e.g. `photos.example.com`.
- **Two object-storage buckets**, on Backblaze B2, Wasabi, Cloudflare R2, or
  any S3-compatible service:
  - a **public** bucket, for photos anyone may view;
  - a **private** bucket, for private photos and database backups.
- **Optional, recommended: a Cloudflare account**, with your domain's DNS on
  Cloudflare. It enables tunnel mode (no open ports on your server) and edge
  caching.

> Photarium is designed for Linux servers. Windows builds exist for edge
> cases (testing, evaluation, unusual environments) and are not recommended
> for production — see section 8.

---

## 2. Choose how HTTPS works

Pick one TLS mode. You pass it to the installer as `PHOTARIUM_TLS`.

| Mode | Use it when | What you do before installing |
|---|---|---|
| `tunnel` (recommended with Cloudflare) | Your domain's DNS is on Cloudflare. | Create a Cloudflare Tunnel and copy its token (below). No ports need to be open on the server. |
| `auto` (default) | No Cloudflare, or you want Photarium reachable directly. | Point the domain's DNS at the server and open ports 80 and 443 to the internet (the installer opens them in ufw/firewalld; cloud firewalls/security groups are up to you). |
| `proxy` | You already run a reverse proxy (nginx, Caddy, …). | Configure your proxy to forward to `http://127.0.0.1:8080`. |

### `tunnel`: create a Cloudflare Tunnel

1. In the Cloudflare dashboard, go to **Networking → Tunnels** and choose
   **Create a tunnel**. (The same tunnels are also listed in Cloudflare One /
   Zero Trust under **Networks → Connectors**.)
2. Name the tunnel (e.g. `photarium`) and create it.
3. Cloudflare shows an install command for `cloudflared` containing a long
   token starting with `eyJ`. Copy **only the token** — that is
   `PHOTARIUM_TUNNEL_TOKEN`. You do not need to run the command; the
   Photarium installer installs `cloudflared` itself. Continue.
4. On the tunnel's **Routes** tab, **Add route → Published application**:
   - Subdomain / Domain: your gallery hostname, e.g. `photos` + `example.com`.
   - Service URL: `http://localhost:8080`.
5. Save. The server needs outbound access to Cloudflare on port 7844; no
   inbound ports.

### `auto`: point DNS at the server

Create an `A` record (and `AAAA` if the server has IPv6) for your domain,
pointing at the server's public IP. If the domain's DNS is on Cloudflare, set
the record to **DNS only** (grey cloud), not Proxied: in `auto` mode Photarium
obtains and renews its own Let's Encrypt certificate and must be reached
directly on ports 80 and 443. If you want Cloudflare in front, use `tunnel`
instead.

Check that DNS has propagated before installing:

```sh
getent hosts photos.example.com     # should print the server's public IP
curl -4 https://icanhazip.com       # the server's public IPv4, for comparison
```

### `proxy`: your own reverse proxy

Photarium listens on `127.0.0.1:8080` over plain HTTP. Your proxy terminates
TLS and forwards to it. Allow request bodies of at least 64 MB, or photo
uploads fail.

Caddy:

```
photos.example.com {
    reverse_proxy 127.0.0.1:8080
}
```

nginx (inside your `server { … }` block for the domain, with TLS configured):

```nginx
client_max_body_size 64m;

location / {
    proxy_pass         http://127.0.0.1:8080;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
}
```

If that proxy is itself behind Cloudflare's proxy (orange cloud), also set
`PHOTARIUM_BEHIND_CLOUDFLARE=yes`, and restrict ports 80/443 to Cloudflare's
IP ranges (section 7).

---

## 3. Set up storage

Photarium keeps every photo in object storage: public photos in the public
bucket (served straight to visitors), private photos and database backups in
the private bucket. Follow the section for your provider; each ends with the
exact installer variables to set.

Bucket names below are examples — bucket names are global on most providers,
so choose your own.

### Backblaze B2

1. Sign in at backblaze.com and open **B2 Cloud Storage → Buckets**.
2. **Create a Bucket** for public photos, e.g. `example-photos-public`.
   Choose **Public** as its privacy setting. Leave Object Lock off.
   - Backblaze only allows a first public bucket once your account has a
     verified email address and payment history on file (or you pay the small
     fee offered in the dialog, which is credited to your balance).
3. **Create a Bucket** for private photos and backups, e.g.
   `example-photos-private`, with privacy set to **Private**.
4. Note the **Endpoint** shown on either bucket, e.g.
   `s3.us-west-004.backblazeb2.com`. The region is the part between `s3.`
   and `.backblazeb2.com` — here `us-west-004`. (A B2 account lives in one
   region, so both buckets share it.)
5. On the public bucket, open **Lifecycle Settings** and choose **Keep only
   the last version of the file**. B2 keeps every old version of a file by
   default, so without this a deleted photo, or one made private, stays stored
   in the public bucket as a hidden version. With it, B2 removes hidden
   versions after about a day. Doing the same on the private bucket stops
   deleted photos and old backups from accumulating.
6. Open **Application Keys → Add a New Application Key**:
   - Name of Key: `photarium-public`
   - Allow access to Bucket(s): the public bucket only
   - Type of Access: **Read and Write**
   - Tick **Allow List All Bucket Names** (Backblaze recommends it for
     S3-compatible clients)
   - Leave the file name prefix and duration empty.
   - **Create New Key**, then copy the **keyID** and **applicationKey**. The
     applicationKey is shown only once.
7. Repeat step 6 for the private bucket (`photarium-private`, private bucket
   only, Read and Write).

Do not use the Master Application Key: B2's S3-compatible API does not
accept it.

Installer variables:

```sh
PHOTARIUM_STORAGE=b2
PHOTARIUM_S3_REGION=us-west-004                    # from the bucket endpoint
PHOTARIUM_PUBLIC_BUCKET=example-photos-public
PHOTARIUM_PRIVATE_BUCKET=example-photos-private
PHOTARIUM_S3_KEY_ID=<keyID of the public-bucket key>
PHOTARIUM_S3_SECRET=<applicationKey of the public-bucket key>
PHOTARIUM_PRIVATE_S3_KEY_ID=<keyID of the private-bucket key>
PHOTARIUM_PRIVATE_S3_SECRET=<applicationKey of the private-bucket key>
```

The public URL is derived for you:
`https://<public-bucket>.s3.<region>.backblazeb2.com`.

Cost note: B2 includes free egress up to three times your average monthly
storage, and egress to Cloudflare is free (Backblaze pricing page, subject to
change).

### Wasabi

1. Choose a region. Wasabi's S3 endpoint for a region is
   `s3.<region>.wasabisys.com` — for example `s3.us-east-1.wasabisys.com`
   (also reachable as `s3.wasabisys.com`), `s3.us-west-1.wasabisys.com`,
   `s3.eu-central-1.wasabisys.com`, `s3.ap-northeast-1.wasabisys.com`. The
   full list is in Wasabi's docs ("service URLs for Wasabi's storage
   regions").
2. In the Wasabi Console, **Buckets → Create Bucket**. Create the public
   bucket (e.g. `example-photos-public`) in your region. Leave **Bucket
   Versioning** and **Object Locking** off (Object Locking cannot be turned
   off later). Create the private bucket (e.g. `example-photos-private`) in
   the same region.
3. **Make the public bucket publicly readable.** Important: Wasabi restricts
   this.
   - Accounts created after March 13, 2023 cannot enable public access
     themselves in the console or CLI; you must ask Wasabi Support
     (support@wasabi.com) to enable it.
   - Trial accounts cannot use public access at all.
   - Paid accounts created before that date can enable it themselves.

   Once public access is available on your account, open the public bucket's
   **Settings → Permissions** tab (called "Policies" in older docs), choose
   **Edit**, paste this policy with your bucket name, and **Save**:

   ```json
   {
     "Version": "2012-10-17",
     "Statement": [
       {
         "Sid": "AllowPublicRead",
         "Effect": "Allow",
         "Principal": { "AWS": "*" },
         "Action": "s3:GetObject",
         "Resource": "arn:aws:s3:::example-photos-public/*"
       }
     ]
   }
   ```

   Wasabi's own example also grants `s3:GetObjectVersion`; it is left out here
   on purpose, so earlier versions of objects can never be read publicly.
   Never apply a public policy to the private bucket.
4. Create an access key: **Access Keys → Create Access Key**, choose **Root
   User** or a **Sub-User**, and **Create**. Download the CSV or copy both
   values — the secret key cannot be retrieved later.
   - Least privilege (recommended): create a policy (**Policies → Create
     Policy**) limited to the two buckets, a sub-user (**Users → Create
     User**) with that policy attached, and the access key for that sub-user:

   ```json
   {
     "Version": "2012-10-17",
     "Statement": [
       { "Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "arn:aws:s3:::*" },
       {
         "Effect": "Allow",
         "Action": "s3:*",
         "Resource": [
           "arn:aws:s3:::example-photos-public",
           "arn:aws:s3:::example-photos-public/*",
           "arn:aws:s3:::example-photos-private",
           "arn:aws:s3:::example-photos-private/*"
         ]
       }
     ]
   }
   ```

Cost notes, from Wasabi's pricing FAQ and docs (check them for current terms):

- Pay-as-you-go storage has a **90-day minimum storage duration**: an object
  deleted sooner is still billed for the remaining days.
- There is a **1 TB minimum** monthly charge.
- Egress is free under a **fair-use policy**: your monthly egress should not
  exceed the amount of data you store. Photarium serves public photos
  straight from the public bucket, so a popular gallery can exceed that;
  Wasabi says use cases that regularly do are not a good fit and may be
  limited.

Installer variables:

```sh
PHOTARIUM_STORAGE=wasabi
PHOTARIUM_S3_REGION=us-east-1                      # your buckets' region
PHOTARIUM_PUBLIC_BUCKET=example-photos-public
PHOTARIUM_PRIVATE_BUCKET=example-photos-private
PHOTARIUM_S3_KEY_ID=<access key>
PHOTARIUM_S3_SECRET=<secret key>
```

The public URL is derived for you:
`https://s3.<region>.wasabisys.com/<public-bucket>`.

### Cloudflare R2

1. In the Cloudflare dashboard, open **R2 object storage** and create two
   buckets, e.g. `example-photos-public` and `example-photos-private` (3–63
   characters: lowercase letters, digits, hyphens). Leave the location
   automatic and do not choose a jurisdiction (see the note at the end).
2. Give the public bucket public access — open it, then **Settings**:
   - **Custom Domains → Add** (recommended): enter a hostname on a zone in
     the same Cloudflare account, e.g. `img.example.com`, then **Continue →
     Connect Domain**. Cloudflare caches it and applies your zone's security
     settings.
   - Or **Public Development URL → Enable** (type `allow` to confirm). This
     gives an `https://pub-….r2.dev` URL that is rate-limited and meant for
     testing only — not for a real gallery.
3. Leave the private bucket as it is: R2 buckets are private by default.
4. Create S3 credentials: on the **R2 object storage** page, next to **API
   Tokens** (under **Account Details**), choose **Manage**, then create a
   **User API token** (or an **Account API token**):
   - Permissions: **Object Read & Write**
   - Scope it to the two buckets only.
   - Create it, then copy the **Access Key ID** and **Secret Access Key**. The
     secret is shown only once.
5. Copy your **Account ID**, shown under **Account Details** on the R2 object
   storage page.
6. If you connected a custom domain, create a cache-purge token as described
   in section 7 ("Image hostname"), so a photo you delete or make private
   stops being served from Cloudflare's cache immediately.

The installer derives the endpoint,
`https://<account-id>.r2.cloudflarestorage.com`, and the region, `auto`.

Installer variables:

```sh
PHOTARIUM_STORAGE=r2
PHOTARIUM_R2_ACCOUNT_ID=<account id>
PHOTARIUM_PUBLIC_BUCKET=example-photos-public
PHOTARIUM_PRIVATE_BUCKET=example-photos-private
PHOTARIUM_S3_KEY_ID=<Access Key ID>
PHOTARIUM_S3_SECRET=<Secret Access Key>
PHOTARIUM_PUBLIC_URL=https://img.example.com       # custom domain or r2.dev URL, no trailing slash
PHOTARIUM_CF_API_TOKEN=<cache purge token>         # recommended with a custom domain
PHOTARIUM_CF_ZONE_ID=<zone id of img.example.com>
```

Jurisdiction note: buckets created in a jurisdiction (EU, FedRAMP) use a
different endpoint, `https://<account-id>.eu.r2.cloudflarestorage.com` (or
`.fedramp.`). The `r2` option derives only the default endpoint, so for those
use the generic `s3` option below with `PHOTARIUM_S3_ENDPOINT` set to the
jurisdiction endpoint.

### Other S3-compatible storage

Any service that speaks the S3 API (AWS S3, MinIO, and others) works. You
need:

- two buckets: the public one must allow anonymous `s3:GetObject`, the
  private one must not;
- an access key with read and write access to both (or one key per bucket,
  using the `PRIVATE_` pair for the private bucket);
- the S3 endpoint URL, and the URL where objects in the public bucket can be
  fetched anonymously.

Installer variables:

```sh
PHOTARIUM_STORAGE=s3
PHOTARIUM_S3_ENDPOINT=https://s3.example-provider.com
PHOTARIUM_S3_REGION=<region, if your provider uses one>
PHOTARIUM_PUBLIC_BUCKET=example-photos-public
PHOTARIUM_PRIVATE_BUCKET=example-photos-private
PHOTARIUM_S3_KEY_ID=<access key>
PHOTARIUM_S3_SECRET=<secret key>
PHOTARIUM_PUBLIC_URL=https://example-photos-public.s3.example-provider.com   # no trailing slash
```

---

## 4. Install

### One-line install

On the server:

```sh
curl -fsSL https://dl.photarium.waxquixotic.com/install.sh | sudo bash
```

It asks for everything it needs — domain, TLS mode, storage provider, keys,
buckets — and finishes by printing your gallery's URL and admin credentials.

### Unattended install (automation and AI agents)

Pass every answer as an environment variable, placed after `sudo`, and set
`PHOTARIUM_NONINTERACTIVE=1` so the installer never prompts and fails clearly
if something required is missing. Example for Backblaze B2 behind a
Cloudflare Tunnel:

```sh
curl -fsSL https://dl.photarium.waxquixotic.com/install.sh | sudo \
  PHOTARIUM_NONINTERACTIVE=1 \
  PHOTARIUM_DOMAIN=photos.example.com \
  PHOTARIUM_TLS=tunnel \
  PHOTARIUM_TUNNEL_TOKEN=eyJhIjoi... \
  PHOTARIUM_STORAGE=b2 \
  PHOTARIUM_S3_REGION=us-west-004 \
  PHOTARIUM_PUBLIC_BUCKET=example-photos-public \
  PHOTARIUM_PRIVATE_BUCKET=example-photos-private \
  PHOTARIUM_S3_KEY_ID=... \
  PHOTARIUM_S3_SECRET=... \
  PHOTARIUM_PRIVATE_S3_KEY_ID=... \
  PHOTARIUM_PRIVATE_S3_SECRET=... \
  bash
```

For another provider, swap in the variables from its section in step 3. For
`auto` mode, drop the two tunnel lines (or set `PHOTARIUM_TLS=auto`) and
optionally add `PHOTARIUM_ACME_EMAIL`.

Already root? Leave out `sudo` and put the variables before `bash`:
`curl -fsSL … | PHOTARIUM_NONINTERACTIVE=1 PHOTARIUM_DOMAIN=… bash`.

Secrets passed this way end up in your shell history. Many shells skip
history for commands that start with a space; otherwise clear the entry
afterwards.

If `PHOTARIUM_ADMIN_PASSWORD` is not set, the installer generates one and
prints it **once**, at the end. Save it.

### Installer variables

| Variable | Required | Meaning |
|---|---|---|
| `PHOTARIUM_DOMAIN` | yes | Public hostname, e.g. `photos.example.com` |
| `PHOTARIUM_TLS` | no (default `auto`) | `auto`: Photarium gets and renews its own Let's Encrypt certificate (ports 80+443 must reach the server directly; the DNS record must NOT be proxied through Cloudflare). `tunnel`: Cloudflare Tunnel — no open ports at all, Cloudflare in front (recommended when using Cloudflare). `proxy`: listen on `127.0.0.1:8080` plain HTTP behind your own reverse proxy. |
| `PHOTARIUM_TUNNEL_TOKEN` | for `tunnel` | The tunnel token from Cloudflare dashboard → Networking → Tunnels → Create a tunnel (the `eyJ…` string in the `cloudflared` install command it shows). |
| `PHOTARIUM_BEHIND_CLOUDFLARE` | no | `yes` if requests arrive through Cloudflare's proxy (implied by `tunnel`); makes Photarium trust `CF-Connecting-IP` for rate limiting and view counts. |
| `PHOTARIUM_ADMIN_USER` | no (default `admin`) | Admin username. |
| `PHOTARIUM_ADMIN_PASSWORD` | no | Admin password; generated and printed once if omitted. |
| `PHOTARIUM_STORAGE` | yes | `b2`, `wasabi`, `r2`, or `s3` (any S3-compatible). |
| `PHOTARIUM_S3_REGION` | b2, wasabi | e.g. `us-west-004` (B2), `us-east-1` (Wasabi). |
| `PHOTARIUM_R2_ACCOUNT_ID` | r2 | Cloudflare account ID. |
| `PHOTARIUM_S3_ENDPOINT` | s3 | Endpoint URL; for the other providers it is derived automatically. |
| `PHOTARIUM_S3_KEY_ID` / `PHOTARIUM_S3_SECRET` | yes | Access key with read/write on both buckets. |
| `PHOTARIUM_PRIVATE_S3_KEY_ID` / `PHOTARIUM_PRIVATE_S3_SECRET` | no | Separate key for the private bucket (recommended on B2, where keys can be restricted to one bucket). |
| `PHOTARIUM_PUBLIC_BUCKET` / `PHOTARIUM_PRIVATE_BUCKET` | yes | Bucket names. |
| `PHOTARIUM_PUBLIC_URL` | r2, s3 (derived for b2/wasabi) | Base URL that serves the public bucket's objects, no trailing slash. |
| `PHOTARIUM_CF_API_TOKEN` / `PHOTARIUM_CF_ZONE_ID` | no | Enables purging Cloudflare's cache when a photo is deleted or made private (only needed if Cloudflare caches your image domain). Token needs Zone → Cache Purge. |
| `PHOTARIUM_ACME_EMAIL` | no | Contact email for Let's Encrypt. |
| `PHOTARIUM_VERSION` | no (default latest) | Install a specific version. |
| `PHOTARIUM_AUTO_UPDATE` | no (default `idle`) | How updates are applied: `idle` (automatically when the site is quiet), `scheduled` (automatically in the hour after 03:00, changeable in Settings), or `off` (only when the admin clicks Update now). |
| `PHOTARIUM_NONINTERACTIVE` | no | `1` = never prompt; fail if something required is missing. |

When the `PRIVATE_` pair is set, `PHOTARIUM_S3_KEY_ID` / `PHOTARIUM_S3_SECRET`
is used for the public bucket.

### What the installer does

1. Detects the distribution and CPU architecture, and installs `curl`,
   `ca-certificates` and `tar` if they are missing.
2. Downloads the release and verifies its SHA-256 checksum.
3. Creates a `photarium` system user.
4. Installs the programs to `/opt/photarium`, writes the configuration to
   `/etc/photarium/photarium.env` (mode `0640`), and keeps data in
   `/var/lib/photarium`.
5. Sets the admin password.
6. For `auto`: opens ports 80 and 443 in ufw or firewalld. For `tunnel`:
   installs `cloudflared` from Cloudflare's package repository and connects
   the tunnel.
7. Tests the storage (`photosctl check`): writes, reads and deletes a small
   test object in each bucket, and fetches it anonymously through the public
   URL. If anything fails it stops here, before starting the server, and
   says what failed — fix `/etc/photarium/photarium.env` and re-run the
   installer.
8. Enables and starts two systemd units: `photarium.service` (the server)
   and `photarium-backup.timer` (a daily database backup to the private
   bucket; the 14 most recent are kept).
9. Waits until the site answers, then prints the URL and the admin
   credentials.

Re-running the installer upgrades the programs in place and keeps the
existing configuration.

When it finishes, open `https://<your domain>` and sign in with the printed
credentials.

---

## 5. Updates

Photarium updates itself. About every six hours it checks for a new release,
and by default it installs one on its own once the site has been quiet for
ten minutes (no uploads or edits in progress). The site restarts for a few
seconds. Before every update:

- a snapshot of the database is saved in `/var/lib/photarium/updates/`;
- the programs being replaced are kept in `/opt/photarium/backup/<version>/`.

If the new version doesn't come up healthy, Photarium puts the old one back
automatically, and won't try that version again on its own.

### Choose how updates happen

Sign in as the admin and open **Settings → Software updates**:

- **Automatically, when the site is quiet** (the default);
- **Automatically, at a set time** — within the hour after the time you pick
  (server time), e.g. 03:00;
- **Only when I click "Update now"** — the page tells you when a version is
  available and installs it when you press the button.

To set the default at install time, use `PHOTARIUM_AUTO_UPDATE=idle`,
`scheduled` or `off` (see the installer variables above).

### How it stays safe

- **Every release is signed.** The release key's public half is built into
  Photarium. Before installing, the server *and* a separate root helper both
  check the signature and the archive's checksum, so a tampered download —
  or a compromised download host — cannot install anything. Downgrades are
  refused.
- **The server never runs as root.** `photosd` downloads and verifies the
  update, then asks a small systemd service (`photarium-update`) to install
  it. That service checks everything again on its own.
- **It rolls back.** After restarting, the helper waits for the new version
  to answer health checks; if it doesn't, the previous programs are restored.

### Privacy, and turning it off

Checking for updates fetches one small file from
`dl.photarium.waxquixotic.com`. Only your server's IP address and a
`photosd/<version>` user agent reach it — nothing about your gallery.

In `/etc/photarium/photarium.env` (then `sudo systemctl restart photarium`):

- `UPDATE_CHECK=false` — never look for updates;
- `SELF_UPDATE=false` — look, and tell the admin, but never install on its own.

### Updating by hand

Re-run the installer. It replaces the programs, keeps
`/etc/photarium/photarium.env` and your data, and restarts the service:

```sh
curl -fsSL https://dl.photarium.waxquixotic.com/install.sh | sudo bash
```

Unattended:

```sh
curl -fsSL https://dl.photarium.waxquixotic.com/install.sh | sudo PHOTARIUM_NONINTERACTIVE=1 bash
```

To move to a specific version, add `PHOTARIUM_VERSION=<version>`. The current
version number is published at
https://dl.photarium.waxquixotic.com/latest/VERSION.

### Going back to the previous version by hand

```sh
sudo systemctl stop photarium
ls /opt/photarium/backup/                      # one folder per previous version
sudo cp /opt/photarium/backup/<version>/{photosd,photosctl,backupctl} /opt/photarium/
sudo systemctl start photarium
```

The database snapshot from just before the update is in
`/var/lib/photarium/updates/`; restore it (with the service stopped) only if
the newer version had changed the database in a way the older one can't read.

---

## 6. Uninstall

There is no uninstall script; these commands remove Photarium but keep your
data.

```sh
sudo systemctl disable --now photarium.service photarium-backup.timer photarium-update.path
for u in photarium.service photarium-backup.service photarium-backup.timer photarium-update.service photarium-update.path; do
  f=$(systemctl show -P FragmentPath "$u"); [ -n "$f" ] && sudo rm -f "$f"
done
sudo systemctl daemon-reload
sudo rm -rf /opt/photarium /etc/photarium
```

If you used `tunnel` mode, also remove the tunnel connector, and delete the
tunnel in the Cloudflare dashboard:

```sh
sudo cloudflared service uninstall
```

Your database and local data in `/var/lib/photarium`, and everything in your
buckets, are kept. Delete them only if you are sure — this cannot be undone:

```sh
sudo rm -rf /var/lib/photarium
sudo userdel photarium
```

The buckets and their contents you delete in your storage provider's console.

---

## 7. Cloudflare recommendations

Setting names below were checked against Cloudflare's documentation in
September 2026; Cloudflare renames and moves settings from time to time.

### Use tunnel mode

`PHOTARIUM_TLS=tunnel` is the recommended setup with Cloudflare. `cloudflared`
makes an outbound connection to Cloudflare, so the server has **no open
inbound ports**: nobody can reach Photarium except through Cloudflare, where
your WAF, rate-limiting and cache rules apply.

### Site hostname (e.g. photos.example.com)

- **SSL/TLS encryption mode — Full (strict)** (SSL/TLS → Overview). Applies
  to `proxy` mode behind Cloudflare: your reverse proxy must present a valid
  certificate for the hostname (a free Cloudflare Origin CA certificate
  works). If the zone shows Automatic SSL/TLS, choose Custom SSL/TLS to set
  Full (strict) explicitly. Not applicable in `tunnel` mode — traffic between
  Cloudflare and `cloudflared` is encrypted independently of this setting —
  or in `auto` mode, where the record is DNS only and Cloudflare is not in the
  path.
- **Always Use HTTPS — On** (SSL/TLS → Edge Certificates). Redirects every
  HTTP request to HTTPS. It applies to every proxied hostname in the zone.
- **HSTS — with caution** (SSL/TLS → Edge Certificates → HTTP Strict
  Transport Security (HSTS)). Tells browsers to use only HTTPS for your
  domain. Start with a short Max Age (1 month), leave *includeSubDomains* and
  *Preload* off unless every subdomain serves HTTPS, and raise it later. While
  it is active, do not switch the record to DNS only or pause Cloudflare, or
  visitors may be locked out.
- **Minimum TLS Version — TLS 1.2** (SSL/TLS → Edge Certificates). Refuses
  outdated TLS 1.0/1.1 clients. (It does not apply to R2 custom domains.)
- **WAF managed rules** (Security → Settings, "Web application exploits").
  The Cloudflare Free Managed Ruleset is on by default on the Free plan. On
  Pro and above, turn on the **Cloudflare Managed Ruleset** as well.
- **Bot Fight Mode — usually leave off** (Security → Settings, "Bot
  traffic"). It can challenge API and automated traffic, and it cannot be
  bypassed with WAF custom rules. That can break scripts or uptime monitors
  that call your gallery, and challenge crawlers that are not on Cloudflare's
  verified-bot list — the very crawlers Photarium's SEO features are built
  for. Turn it on only if bots are a real problem, then test signing in and
  uploading.
- **Do not cache `/api/*`** (Caching → Cache Rules → Create rule). Filter:
  *URI Path* *starts with* `/api/` — expression
  `starts_with(http.request.uri.path, "/api/")` — then **Cache eligibility:
  Bypass cache**, and Deploy. API responses are per-user: signed-in admin
  data and private photos are served through `/api/`, and must never be
  stored in a shared edge cache. This rule also protects you from any broader
  "cache everything" rule added later.
- **Compression is automatic.** Cloudflare retired the Brotli toggle and
  compresses responses automatically (Brotli or Zstandard, depending on
  plan). Nothing to set.

### Image hostname (a Cloudflare-cached public bucket, e.g. img.example.com)

This applies when Cloudflare serves your public bucket — an R2 custom domain.

- **Cache everything with a long edge TTL** (Caching → Cache Rules): filter
  *Hostname* *equals* `img.example.com`; **Cache eligibility: Eligible for
  cache**; **Edge TTL: Ignore cache-control header and use this TTL** — e.g.
  1 month when a purge token is configured (next item), or 1 day without
  one. Keep **Browser TTL** modest (e.g. a few hours): Cloudflare can purge
  its own cache, but not visitors' browsers.
- **Tiered Cache** (Caching → Tiered Cache → Smart Tiered Cache). Free on
  all plans. Cloudflare data centers fetch from a nearby upper tier instead of
  each going to your bucket, so fewer requests reach storage. Cloudflare
  recommends it for R2.
- **Cache Reserve — optional** (Caching → Cache Reserve). A paid,
  usage-billed add-on that keeps cached photos in persistent storage so
  rarely viewed ones stay cached. Worth considering for large libraries.
- **Set a purge token — this is about privacy.** When you delete a photo or
  make it private, Photarium removes it from the public bucket, but copies
  already cached by Cloudflare would keep being served until they expire.
  With a token, Photarium purges them immediately.
  1. **My Profile → API Tokens → Create Token → Create Custom Token.**
  2. Permissions: **Zone → Cache Purge → Purge**.
  3. Zone Resources: **Include → Specific zone →** the image domain's zone.
  4. Create it and copy the token: `PHOTARIUM_CF_API_TOKEN`.
  5. The zone ID is on the zone's **Overview** page, in the API section:
     `PHOTARIUM_CF_ZONE_ID`.

### Rate-limit sign-in attempts

Photarium limits login attempts itself; a Cloudflare rule stops password
guessing at the edge, before it reaches your server.

Security → Security rules → Create rule → **Rate limiting rules**:

- If incoming requests match: `http.request.uri.path eq "/api/auth/login"`
- With the same characteristics: IP
- When rate exceeds: e.g. 5 requests per 10 seconds (Free), or 10 per minute
  (Pro and above)
- Then: **Block**, for the longest duration your plan allows (10 seconds on
  Free, up to 1 hour on Pro).

On Business and Enterprise you can narrow it to sign-in submissions only:
`http.request.uri.path eq "/api/auth/login" and http.request.method eq "POST"`.
The Free and Pro plans cannot match on request method, so use the path-only
rule there. (Free allows one rate-limiting rule.)

### Proxy without a tunnel: allow only Cloudflare

If you use `proxy` mode behind Cloudflare's proxy (orange cloud) instead of a
tunnel, allow ports 80/443 only from Cloudflare's IP ranges. Otherwise anyone
can reach the server directly, around your WAF and rate limits — and because
`PHOTARIUM_BEHIND_CLOUDFLARE=yes` trusts the `CF-Connecting-IP` header, a
direct request could claim any visitor address. With ufw:

```sh
# if you opened 80/443 to everyone before, remove those rules first:
sudo ufw delete allow 80/tcp
sudo ufw delete allow 443/tcp

for ip in $(curl -fsSL https://www.cloudflare.com/ips-v4) $(curl -fsSL https://www.cloudflare.com/ips-v6); do
  sudo ufw allow proto tcp from "$ip" to any port 80,443
done
```

Cloudflare's ranges are published at https://www.cloudflare.com/ips/ and
change occasionally; re-run the loop now and then. (Tunnel mode needs none
of this — nothing is open.)

---

## 8. Windows (edge cases)

> Photarium is designed for Linux servers. The Windows build is for testing,
> evaluation and unusual environments, and is not recommended for production.

1. Download `photarium-windows-amd64.zip` from
   https://photarium.waxquixotic.com/download and verify it (the download
   page shows how).
2. Unzip it to a folder, e.g. `C:\Photarium`.
3. In the folder that holds `photosd.exe`, copy `photarium.env.example` to
   `photarium.env` and fill it in — the comments in the file explain each
   setting. `photosd` reads `photarium.env` from next to itself
   automatically.
4. Set the admin password, then start the server:

```powershell
cd C:\Photarium
Copy-Item photarium.env.example photarium.env
notepad photarium.env
.\photosctl.exe set-password admin
.\photosd.exe
```

To run it at boot, use a service wrapper such as NSSM (`nssm install
Photarium C:\Photarium\photosd.exe`, with the startup directory set to
`C:\Photarium`), or a Task Scheduler task triggered **At startup** that runs
`photosd.exe` with **Start in** set to its folder and "Run whether user is
logged on or not" selected. Schedule `backupctl.exe` the same way if you
want database backups.

---

## 9. Troubleshooting

Follow the server's log live, and check whether it is running:

```sh
journalctl -u photarium -f
systemctl status photarium
```

**Certificate errors in `auto` mode.** Photarium could not get a Let's
Encrypt certificate. Usually one of:

- DNS does not point at the server yet — compare `getent hosts
  photos.example.com` with the server's public IP, and wait for propagation;
- port 80 (or 443) is blocked — check ufw/firewalld and any cloud firewall or
  security group;
- the DNS record is proxied through Cloudflare (orange cloud). Set it to DNS
  only, or better, re-run the installer with `PHOTARIUM_TLS=tunnel`.

**Storage errors** (uploads fail, images missing, errors mentioning the bucket
in the log). Check the keys and bucket names in
`/etc/photarium/photarium.env`, fix them, then restart:

```sh
sudo nano /etc/photarium/photarium.env
sudo -u photarium PHOTARIUM_CONFIG=/etc/photarium/photarium.env /opt/photarium/photosctl check
sudo systemctl restart photarium
```

`photosctl check` tests each bucket (write, read, delete) and the public URL,
and names the step that fails.

Common causes: a B2 key restricted to the other bucket, the B2 Master
Application Key (not supported), the wrong region, or — on Wasabi — public
access not yet enabled on the account, which leaves public images
unreadable.

**Tunnel mode: site unreachable.** Check the connector and its log, and that
the route's Service URL is `http://localhost:8080`:

```sh
systemctl status cloudflared
journalctl -u cloudflared -f
```

**Backups.** See when the backup last ran and what it said:

```sh
systemctl list-timers photarium-backup.timer
journalctl -u photarium-backup
```

---

Photarium is free software under the GNU Affero General Public License v3.0
(https://www.gnu.org/licenses/agpl-3.0.html). Copyright © 2026 Scott Alan
Miller and NTG LLC | Quixotic Systems.
Source code: https://dl.photarium.waxquixotic.com/latest/photarium-source.tar.gz
