## 1. Project Overview & Quickstart (LycheeOrg/Contribute)

## 1. Project Overview & Quickstart (LycheeOrg/Contribute)

# Contribute

Open-source repository LycheeOrg/Contribute

### Repository Details
- **Repository:** [LycheeOrg/Contribute](https://github.com/LycheeOrg/Contribute)
- **Primary Language:** Code

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (LycheeOrg/LycheeOrg.github.io)

# lycheeorg.dev

[
[

## Dependencies

- GNU Make
- npm
- Python 3 (because I don't like Python 2.7)
- git

## Setup

- [Install Python 3.x](https://www.python.org/downloads/)
- [Install pip](https://pip.pypa.io/en/stable/installing/)
- [Install npm](https://nodejs.org/en/download/package-manager)
- Install dependencies:

```sh
npm install
pip install -r requirements.txt
```

## Local dev

In order to get the visual feedback of the website (without docs), you can run a local version:

```sh
npm run dev
# Open http://localhost:4321/
```

# Generating

Run `make` to generate the HTML files, which can then be found in `dist/`.

[build-status-shield]: https://img.shields.io/github/actions/workflow/status/LycheeOrg/LycheeOrg.github.io/CI.yml?branch=master
[image-status-shield]: https://img.shields.io/github/actions/workflow/status/LycheeOrg/LycheeOrg.github.io/Docker.yml?branch=master

## 2. Official Technical Reference & Guides (LycheeOrg/LycheeOrg.github.io)

## File: README.md

# lycheeorg.dev

[
[

## Dependencies

- GNU Make
- npm
- Python 3 (because I don't like Python 2.7)
- git

## Setup

- [Install Python 3.x](https://www.python.org/downloads/)
- [Install pip](https://pip.pypa.io/en/stable/installing/)
- [Install npm](https://nodejs.org/en/download/package-manager)
- Install dependencies:

```sh
npm install
pip install -r requirements.txt
```

## Local dev

In order to get the visual feedback of the website (without docs), you can run a local version:

```sh
npm run dev
# Open http://localhost:4321/
```

# Generating

Run `make` to generate the HTML files, which can then be found in `dist/`.

[build-status-shield]: https://img.shields.io/github/actions/workflow/status/LycheeOrg/LycheeOrg.github.io/CI.yml?branch=master
[image-status-shield]: https://img.shields.io/github/actions/workflow/status/LycheeOrg/LycheeOrg.github.io/Docker.yml?branch=master

---

## File: AGENTS.md

# AstroWind Agent Instructions

## Project Overview

AstroWind is a free, open-source website template built with **Astro v6** and **Tailwind CSS v4**. It generates a fully static site optimized for performance, SEO, and accessibility.

**Stack:** Astro v6 | Tailwind CSS v4 | TypeScript 5.9 | MDX | Sharp

## Quick Reference

| Command           | Purpose                             |
| ----------------- | ----------------------------------- |
| `npm run dev`     | Start dev server at localhost:4321  |
| `npm run build`   | Production build to `./dist/`       |
| `npm run preview` | Preview production build locally    |
| `npm run check`   | Run astro check + ESLint + Prettier |
| `npm run fix`     | Auto-fix ESLint + Prettier issues   |

**Node.js requirement:** >= 22.12.0

## Architecture

### Directory Structure

```
src/
  assets/styles/tailwind.css   # Tailwind v4 config (themes, utilities, plugins)
  components/
    common/        # Shared: Image, Metadata, Analytics, ToggleTheme
    ui/            # Primitives: Button, Headline, WidgetWrapper, ItemGrid
    widgets/       # Page sections: Hero, Features, Pricing, Header, Footer
    blog/          # Blog: SinglePost, List, Pagination, Tags
    CustomStyles.astro  # CSS variables for colors and fonts
  content.config.ts    # Content Collections schema (Astro v6 location)
  data/post/           # Blog posts (.md, .mdx)
  layouts/             # Layout.astro, PageLayout.astro, MarkdownLayout.astro
  pages/               # File-based routing
  utils/               # blog.ts, images.ts, permalinks.ts, frontmatter.ts
  config.yaml          # Site configuration (loaded as virtual module)
  navigation.ts        # Navigation structure
  types.d.ts           # TypeScript type definitions
vendor/integration/    # Custom Astro integration for config loading
```

### Path Aliases

Use `~/` to import from `src/`:

```typescript
import Image from '~/components/common/Image.astro';
import { SITE } from 'astrowind:config';
```

### Configuration System

Site config lives in `src/config.yaml` and is loaded as a Vite virtual module `astrowind:config` by the custom integration in `vendor/integration/`. Exports: `SITE`, `I18N`, `METADATA`, `APP_BLOG`, `UI`, `ANALYTICS`.

## Tailwind CSS v4

Configuration is CSS-first in `src/assets/styles/tailwind.css`:

- **Theme tokens:** `@theme { --color-primary: var(--aw-color-primary); ... }`
- **Custom utilities:** `@utility bg-page { ... }`
- **Dark mode:** Class-based via `@variant dark (&:where(.dark, .dark *))`
- **Plugins:** `@plugin "@tailwindcss/typography"`
- **Custom variant:** `@custom-variant intersect (&:not([no-intersect]))`

CSS variables for colors/fonts are defined in `src/components/CustomStyles.astro` with light/dark theme variants.

The Vite plugin `@tailwindcss/vite` is configured in `astro.config.ts` (not as an Astro integration).

### Class Merging

Components use `twMerge` from `tailwind-merge` v3 for conditional class composition.

## Content Collections

Defined in `src/content.config.ts` using the Astro v6 Content Layer API with `glob()` loader. Posts are in `src/data/post/` as `.md` or `.mdx` files.

Post frontmatter: `title` (required), `publishDate`, `updateDate`, `draft`, `excerpt`, `image`, `category`, `tags`, `author`, `metadata`.

## Component Patterns

- Props extend interfaces from `~/types`
- Use `class:list` for conditional classes
- Use `twMerge()` when accepting className overrides
- Use named slots for layout composition
- Widget components accept standardized props (see `~/types`)

## Image Handling

`src/components/common/Image.astro` supports:

- Local images via `astro:assets` (optimized by Sharp)
- Remote images via Unpic CDN
- Allowed domains (for providers Unpic can't detect, processed by Sharp): `cdn.pixabay.com`

Hero images use `loading="eager"` and `fetchpriority="high"`.

## Verification Checklist

After changes, always verify:

1. `npm run build` succeeds
2. `npm run check` passes (astro check + ESLint + Prettier)
3. Visual check in browser: homepage, blog, dark mode, mobile menu

---

## File: CLAUDE.md

# Claude Code Configuration

See [AGENTS.md](./AGENTS.md) for all project documentation and AI agent instructions.

---

## File: LICENSE.md

MIT License

Copyright (c) 2018-2024 LycheeOrg
Copyright (c) 2023 onWidget (for astro parts)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

---

## File: package.json

{
  "name": "@onwidget/astrowind",
  "version": "1.0.0-beta.63",
  "description": "AstroWind: A free template using Astro 6 and Tailwind CSS v4. Astro starter theme.",
  "type": "module",
  "private": true,
  "engines": {
    "node": ">=22.12.0"
  },
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro",
    "check": "npm run check:astro && npm run check:eslint && npm run check:prettier",
    "check:astro": "astro check",
    "check:eslint": "eslint .",
    "check:prettier": "prettier --check .",
    "fix": "npm run fix:eslint && npm run fix:prettier",
    "fix:eslint": "eslint --fix .",
    "fix:prettier": "prettier -w ."
  },
  "dependencies": {
    "@astrojs/markdown-satteri": "^0.4.2",
    "@astrojs/rss": "^4.0.19",
    "@astrojs/sitemap": "^3.7.3",
    "@astrojs/starlight": "^0.42.5",
    "@astrojs/starlight-tailwind": "^5.0.0",
    "@fontsource-variable/inter": "^5.2.8",
    "astro": "^7.3.3",
    "astro-embed": "^0.13.0",
    "astro-icon": "^1.1.5",
    "astro-seo": "^1.1.0",
    "limax": "4.2.3",
    "lodash.merge": "^4.6.2",
    "prismjs": "^1.30.0",
    "satteri": "^0.10.5",
    "unpic": "^4.2.2"
  },
  "devDependencies": {
    "@astrojs/check": "^0.9.9",
    "@astrojs/mdx": "^8.0.2",
    "@astrojs/partytown": "^2.1.7",
    "@eslint/js": "^10.0.1",
    "@iconify-json/flat-color-icons": "^1.2.3",
    "@iconify-json/tabler": "^1.2.35",
    "@tailwindcss/typography": "^0.5.19",
    "@tailwindcss/vite": "^4.3.0",
    "@types/js-yaml": "^4.0.9",
    "@types/lodash.merge": "^4.6.9",
    "@types/mdx": "^2.0.13",
    "@types/prismjs": "^1.26.5",
    "@typescript-eslint/eslint-plugin": "^8.60.0",
    "@typescript-eslint/parser": "^8.60.0",
    "astro-compress": "^2.4.1",
    "astro-eslint-parser": "^1.4.0",
    "eslint": "^10.4.0",
    "eslint-plugin-astro": "^1.7.0",
    "globals": "^17.6.0",
    "js-yaml": "^4.3.2",
    "prettier": "^3.8.3",
    "prettier-plugin-astro": "^0.14.1",
    "reading-time": "^1.5.0",
    "sharp": "0.35.4",
    "tailwind-merge": "^3.6.0",
    "tailwindcss": "^4.3.0",
    "typescript": "^5.9.3",
    "typescript-eslint": "^8.60.0"
  },
  "overrides": {
    "yaml": ">=2.8.3"
  }
}

---

## File: sandbox.config.json

{
  "infiniteLoopProtection": true,
  "hardReloadOnChange": false,
  "view": "browser",
  "template": "node",
  "container": {
    "port": 3000,
    "startScript": "start",
    "node": "18"
  }
}

---

## File: tsconfig.json

{
  "extends": "astro/tsconfigs/base",
  "compilerOptions": {
    "strictNullChecks": true,
    "allowJs": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["src/*"]
    }
  },
  "include": [".astro/types.d.ts", "**/*"],
  "exclude": ["dist/"]
}

---

## File: vscode.tailwind.json

{
  "version": 1.1,
  "atDirectives": [
    {
      "name": "@tailwind",
      "description": "@tailwind tailwindcss"
    },
    {
      "name": "@layer",
      "description": "@layer tailwindcss"
    },
    {
      "name": "@apply",
      "description": "@apply tailwindcss"
    }
  ]
}

---

## File: public/robots.txt

User-agent: *
Disallow:

---

## File: public/decapcms/config.yml

backend:
  name: github
  repo: LycheeOrg/LycheeOrg.github.io # Path to your GitHub repository

media_folder: 'src/assets/images'
public_folder: '/_astro'

collections:
  - name: 'post'
    label: 'Post'
    folder: 'src/content/post'
    create: true
    fields:
      - { label: 'Title', name: 'title', widget: 'string' }
      - { label: 'Excerpt', name: 'excerpt', widget: 'string' }
      - { label: 'Category', name: 'category', widget: 'string' }
      - {
          label: 'Tags',
          name: 'tags',
          widget: 'list',
          allow_add: true,
          allow_delete: true,
          collapsed: false,
          field: { label: 'Tag', name: 'tag', widget: 'string' },
        }
      - { label: 'Image', name: 'image', widget: 'string' }
      - { label: 'Publish Date', name: 'publishDate', widget: 'datetime', required: false }
      - { label: 'Author', name: 'author', widget: 'string' }
      - { label: 'Content', name: 'body', widget: 'markdown' }

---

## File: src/config.yaml

site:
  name: Lychee
  site: 'https://lycheeorg.dev'
  base: '/'
  trailingSlash: ignore

  # googleSiteVerificationId: orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M

# Default SEO metadata
metadata:
  title:
    default: Lychee
    template: 'Lychee - %s'
  robots:
    index: true
    follow: true
  openGraph:
    site_name: Lychee
    images:
      - url: '~/assets/images/og.jpg'
        width: 1200
        height: 628
    type: website
  # twitter:
  #   handle: '@onwidget'
  #   site: '@onwidget'
  #   cardType: summary_large_image

i18n:
  language: en
  textDirection: ltr

apps:
  blog:
    isEnabled: true
    postsPerPage: 10

    post:
      isEnabled: true
      permalink: '/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
      robots:
        index: true

    list:
      isEnabled: true
      pathname: 'blog' # Blog main path, you can change this to "articles" (/articles)
      robots:
        index: true

    category:
      isEnabled: true
      pathname: 'category' # Category main path /category/some-category, you can change this to "group" (/group/some-category)
      robots:
        index: true

    tag:
      isEnabled: true
      pathname: 'tag' # Tag main path /tag/some-tag, you can change this to "topics" (/topics/some-category)
      robots:
        index: false

    isRelatedPostsEnabled: true
    relatedPostsCount: 4

# analytics:
#   vendors:
#     googleAnalytics:
#       id: null # or "G-XXXXXXXXXX"

ui:
  theme: 'system' # Values: "system" | "light" | "dark" | "light:only" | "dark:only"

---

## File: src/content/docs/docs/administration/advanced-setups.md

---
title: "Advanced Setups"
sidebar:
  order: 3
---

In this page we describe additional ways to install and potentially to develop on Lychee.

## Separating Data from the Code

As described in issue #486, LudovicRousseau proposes a solution to separate your photos from the Lychee code.

He splits Lychee itself and the pictures into 2 directories:
- `Lychee` with the code of Lychee
- `Lychee-data` containing:
```
Lychee-data/
└── public
    ├── dist
    │   ├── fonts
    │   └── resources
    └── uploads
        ├── big
        ├── import
        ├── medium
        ├── raw
        ├── small
        └── thumb
```
- his `Lychee/.env` contains:
```ini
LYCHEE_DIST="/var/www/Lychee-data/public/dist/"
LYCHEE_UPLOADS="/var/www/Lychee-data/public/uploads/"
```

:::note
Note that the **path needs to be absolute**.
:::



The upgrade script when a new version of Lychee is available is simple:
```bash
mv Lychee Lychee.old
unzip Lychee.zip
cp Lychee.old/.env Lychee
chmod g+w -R Lychee/storage
sudo chgrp www-data -R Lychee/storage
cd Lychee
php artisan key:generate
php artisan migrate
```
It worked fine with the migration from 4.0.4 to 4.0.5 of Lychee.

The hierarchy `Lychee-data/public/` is writeable by the group `www-data` so that apache process can modify the files.
The files in `Lychee` are read only for apache. No write access needed.


## Other means of distributions

In this section we present few out-of-the-box solutions providing Lychee.

:::note
Please note that we, at LycheeOrg, do not endorse any of the following solutions.
:::

We give the reader a link to their websites as they provide our software.
**You are free to use their installation, however we will not be providing you support with their solution**.

### With Linuxserver.io (Free)

Linuxserver.io is a group of like minded enthusiasts from across the world who build and maintain the largest collection of Docker images on the web,
and at our core are the principles behind Free and Open Source Software.
Their primary goal is to provide easy-to-use and streamlined Docker images with clear and concise documentation.

You can find the corresponding Lychee image [here](https://fleet.linuxserver.io/image?name=linuxserver/lychee).

### With Yunohost (Free)

[Yunohost](https://yunohost.org/) is a simple operating system for self-hosting your applications.
[Lychee is available from their app store](https://yunohost.org/de/app_lychee) and can be installed by clicking [this installation link](https://install-app.yunohost.org/?app=lychee).
You can find the source code of the app under <https://github.com/YunoHost-Apps/lychee_ynh>.

### With Cloudron (Paid)

Cloudron is a complete solution for running apps on your server and keeping them up-to-date and secure.

[](https://cloudron.io/store/com.electerious.lychee.cloudronapp.html)

The source code for the package can be found [here](https://git.cloudron.io/cloudron/lychee-app).

See their pricing [here](https://cloudron.io/pricing.html).

### With alwaysdata (Paid)

alwaysdata.com is a modern hosting platform allowing you to host all your services in one place: files, emails, sites, media, databases, workers, etc.

You can use their Marketplace to [install Lychee](https://www.alwaysdata.com/en/marketplace/lychee/) and have your own instance available (with HTTPS) within a minute!

See their pricing [here](https://www.alwaysdata.com/en/).

### With Kubernetes (Free)

You can use a Community maintained Helmchart to deploy Lychee into your Kubernetes Cluster.

Have a look at the [Repository](https://github.com/breuerfelix/lychee-helmchart) for installation instructions.

Feel free to open an [Issue](https://github.com/breuerfelix/lychee-helmchart/issues) if you need help.

### With Portainer on Synology (Free~ish)

[Marius Bogdan Lixandru](https://mariushosting.com/author/marius/) made a great post of how to set up your Lychee installation on your Synology NAS with Portainer. Read at it [here](https://mariushosting.com/how-to-install-lychee-on-your-synology-nas/).

### With Zenith (Paid)

Zenith provides one-click managed hosting for open-source apps, so you can bring up a fully managed Lychee instance in minutes without any server setup.

[](https://zenith.hosting/host/lychee)

You can find their Lychee offering [here](https://zenith.hosting/host/lychee).

## Implementing Object Storage 

An S3-compatible object storage solution is designed to store, manage, and access unstructured data in the cloud. 

Under Object Storage, files (also called objects) are stored in flat data structures (referred to as buckets) alongside their own rich metadata.

Due to the nature of Object Storage, it does not require the use of a Compute Instance. Instead, Object Storage gives each object a unique URL with which you can access the data.

### Limitations
- Upload file size limit: 5 GB
- All recursive tasks including (search, find, ls, etc.) have poor performance due to network latency

### S3 Object Storage Setup

The instructions in this guide are based on CentOS/Fedora/RHEL family, but it is easy to replicate the commands into any other distro.


**Install s3fs FUSE-base file system**

s3fs allows Linux and other OS's to mount an S3 bucket via FUSE (Filesystem in Userspace).

s3fs makes you operate files and directories in S3 bucket like a local file system.

[s3fs-fuse - Instructions from developer](https://github.com/s3fs-fuse/s3fs-fuse)

```bash
sudo dnf install s3fs-fuse
sudo echo ACCESS_KEY_ID:SECRET_ACCESS_KEY > /etc/passwd-s3fs
sudo chmod 600 /etc/passwd-s3fs
```

**Configure SELinux to allow access to the new filesystem**

```bash
setsebool -P httpd_use_fusefs 1
```

**Setup a permanent mounting point**

Create S3 Object mount point:

```bash
sudo mkdir /mnt/bucket
```

Edit fstab to create a new mount on boot:  
Add the following line to: `/etc/fstab`
```bash
<bucket> /mnt/bucket fuse.s3fs _netdev, allow_other, enable_noobj_cache, url=<s3_endpoint>, use_cache="", passwd_file=/etc/passwd-s3fs, mp_umask=0002 0 0
```

Example:

-                         mybucket (name used in your cloud provider)
- <s3_endpoint>                   https://eu-central-1.linodeobjects.com

Parameters are explained below:

- `allow_other`                     Allow other users to access the bucket
- `mp_umask`                        Mask permissions for mount point
- `enable_noobj_cache`              Performance improvement - Enable when bucket is exclusively used by s3fs
- `use_cache=""`                    Disabled
- `use_cache=/var/cache/s3fs`       Enabled (to be used with care, because the cache can grow out of control. Also, I haven't noticed much difference using it)
	

Reboot your server to confirm S3 Object Storage is mounted correctly.  
Create Lychee's mount point:
```bash
sudo mkdir /mnt/bucket/uploads
```

### Create and run Lychee container
From now on, Lychee will see the Object Storage mount transparently like any other mount.  
The container's volume `/uploads` needs to point to the new created mount:

```bash
sudo podman run --rm -d --name myphotos -v /mnt/bucket/uploads:/uploads -e PUID=33 -e PGID=1000 ... docker.io/lycheeorg/lychee
```

### Configure .ENV
To avoid latency when clicking Diagnostics, my suggestion is to disable BasicPermissionCheck. Otherwise, depending on the number of photos in your gallery, this task can take hours.

```bash
# /var/lib/containers/storage/volumes/lychee-conf/_data/.env
SKIP_DIAGNOSTICS_CHECKS=BasicPermissionCheck
```

### Limitations to be considered
As explained before, recursive tasks are penalised in Object Storage, so if you have an existing bucket and the container runs for the first time, it will take long time to review and set the permissions in your mount. Depending on the number of photos, it can take several hours.

## Hosting Lychee in a subpath with Apache

To serve Lychee in a subpath, e.g. `https://my.url/photos/`, we can use Apaches's [Alias](https://httpd.apache.org/docs/2.4/mod/mod_alias.html#alias)-directive.

In this example we assume the web root to be located at `/var/www/html`, the web user and group to be `www-data`, and install [from the master branch](/docs/getting-started/manual-installation/#from-source).

Before getting started, make sure your system qualifies the [Server Requirements](/docs/getting-started/manual-installation/#server-requirements). 

### Setup Lychee

Clone the repository to outside the web root, e.g. `/var/www/`:

```bash
sudo git clone https://www.github.com/LycheeOrg/Lychee /var/www/
```

Set the file ownership and permissions:

```bash
sudo chown -R www-data:www-data /var/www/Lychee
sudo chmod -R 2775 /var/www/Lychee
```

Install composer in `/var/www/Lychee` as instructed in [here](https://getcomposer.org/download/).
Then, from `/var/www/Lychee`, install the project dependencies:

```bash
sudo -u www-data php composer.phar install --no-dev
```

### Setup Apache

Link `https://my.url/photos/` to `/var/www/Lychee/public` by adding the following to your Apache configuration for Aliases.
In Apache 2.4 running on Debian, the correct configuration file is `/etc/apache2/mods-available/alias.conf`.

```apache
Alias /photos /var/www/Lychee/public

<Directory /var/www/Lychee/public>
   Options FollowSymLinks
   AllowOverride All
   Require all granted
</Directory>
```

Finally, restart Apache:

```bash
sudo service apache2 restart
```

### Configure Lychee

At this point, you should be able to go to `https://my.url/photos/` and run the web installer.
During the installation procedure, on top of adding the database credentials to match your setup, set your `APP_URL` to the served subdirectory:

```bash
APP_URL=https://my.url/photos/
```

Done!

---

## File: src/content/docs/docs/administration/api.md

---
title: "API"
sidebar:
  order: 6
---

The current API provide the following entry points:
See:

- [routes/api_v2.php](https://github.com/LycheeOrg/Lychee/blob/master/routes/api_v2.php)
- [routes/web-admin-v2.php](https://github.com/LycheeOrg/Lychee/blob/master/routes/web-admin-v2.php)
- [routes/web_v2.php](https://github.com/LycheeOrg/Lychee/blob/master/routes/web_v2.php)

Note that for all request, **'Accept: application/json' is mandatory** and that without mention of the contrary **'Content-Type: application/json' is also mandatory**.

As of version 4.8.1, the api documentation is moved directly inside your own Lychee instance. It is accessible at the url `https://yourLycheeInstance.org/docs/api`.
It is also possible to see it on our demo website: [https://demo.lycheeorg.dev/docs/api](https://demo.lycheeorg.dev/docs/api)

## Authentication and CSRF

Lychee protects all stateful (cookie/session based) requests with Laravel's standard CSRF protection. This is what your browser uses automatically when you are logged in and is required for `POST`, `PUT`, `PATCH` and `DELETE` requests coming from a session.

When you script against the API (e.g. with `curl`, Python, or any external tool) you generally do not have a browser session or the associated CSRF cookie/header, so plain session-based requests will be rejected with a `419` error.

To avoid dealing with CSRF for such use cases, Lychee lets you authenticate with a personal **API token** instead. Any request that carries a valid `Authorization: Bearer <token>` header is treated as token-authenticated and skips CSRF verification entirely, regardless of the HTTP verb used.

### Generating a token

1. Log in to your Lychee instance and open **Settings → Profile**.
2. In the **API Token** section, click **Create token** (or **Reset token** if one already exists).
3. Copy the token immediately — for security reasons it is only ever shown once and cannot be retrieved again later.
4. You can revoke the token at any time from the same dialog with **Disable token**. Resetting the token also invalidates the previous one.

### Using the token

Send the token as a Bearer token in the `Authorization` header of your requests:

```bash
curl -X POST 'https://yourLycheeInstance.org/api/v2/Albums' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <your-token-here>'
```

:::caution
An API token grants full access to your account. Treat it like a password: never commit it to source control and revoke it immediately if you suspect it has leaked.
:::

---

## File: src/content/docs/docs/administration/external-tracking.md

---
title: "External Tracking"
sidebar:
  order: 4
---

This guide will help you to set up Matomo, Google Analytics or other analytics tools with Lychee.


> ⚠️ Make sure to use your actual values for domain and siteId

### Allow loading tracking script

Set the following env vars:

```ini
SECURITY_HEADER_SCRIPT_SRC_ALLOW=https://analytics.example.com/matomo.js
SECURITY_HEADER_CSP_CONNECT_SRC=https://analytics.example.com/matomo.php
```

### Add custom JavaScript

Add the following to Settings > Advanced Customization

```javascript
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function () {
    var u = "https://analytics.example.com/";
    _paq.push(['setTrackerUrl', u + 'matomo.php']);
    _paq.push(['setSiteId', '<site-id>']);
    var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
    g.async = true;
    g.src = u + 'matomo.js';
    s.parentNode.insertBefore(g, s);
})();

navigation.addEventListener("navigate", function (e) {
    _paq.push(['setDocumentTitle', document.title]);
    _paq.push(['trackPageView']);
});
```

## Set up for Medama

> ⚠️ Make sure to use your actual domain

### Allow loading tracking script

Set the following env vars:

```ini
SECURITY_HEADER_SCRIPT_SRC_ALLOW=https://analytics.example.com
SECURITY_HEADER_CSP_CONNECT_SRC=https://analytics.example.com/script.js
```

### Add custom JavaScript

Add the following to Settings > Advanced Customization

```javascript
const script = document.createElement('script');
script.setAttribute('defer', true);
script.setAttribute('src', 'https://analytics.example.com/script.js');
document.body.appendChild(script);
```

## Set up for Google Analytics

> ⚠️ Make sure to replace &lt;your-tracking-id&gt; with the actual value

### Allow loading tracking script

Set the following env vars:

```ini
SECURITY_HEADER_SCRIPT_SRC_ALLOW=https://www.googletagmanager.com/gtag/js?id=<your-tracking-id>
SECURITY_HEADER_CSP_CONNECT_SRC=https://www.googletagmanager.com/gtag/js?id=<your-tracking-id>
```

### Add custom JavaScript

Add the following to Settings > Advanced Customization

```javascript
(function () {
  var u = "https://www.googletagmanager.com/gtag/js";
  var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
  g.async = true;
  g.src = u + '?id=<your-tracking-id>';
  s.parentNode.insertBefore(g, s);
})();

window.dataLayer = window.dataLayer || [];
function gtag(){
    dataLayer.push(arguments);
}

function trackPageView() {
  gtag('event', 'page_view', {
    page_title: document.title,
    page_location: window.location
  });
}

gtag('js', new Date());
gtag('config', '<your-tracking-id>', {
  send_page_view: false
});
trackPageView();

navigation.addEventListener("navigate", function (e) {
  trackPageView()
});
```
## Set up for Umami Analytics

> ⚠️ Make sure to use your actual values for DOMAIN SCRIPT URL and <WEBSITE_ID>

### Allow loading tracking script

Set the following env vars:

```ini
SECURITY_HEADER_SCRIPT_SRC_ALLOW=https://umami.example.com/script.js
SECURITY_HEADER_CSP_CONNECT_SRC=https://umami.example.com/script.js
```

### Add custom JavaScript

Add the following to Settings > Advanced Customization

```javascript
(function() {
    var d = document, s = d.createElement('script'), g = d.getElementsByTagName('script')[0];
    s.defer = true;
    s.src = 'https://umami.example.com/script.js';
    s.setAttribute('data-website-id', '<WEBSITE_ID>');
    g.parentNode.insertBefore(s, g);
})();

navigation.addEventListener("navigate", function (e) {
    if (window.umami && typeof umami.trackView === 'function') {
        umami.trackView(document.title, window.location.pathname);
    }
});
```

## Other tools / custom tracking

### Configuration adjustments

To make the frontend load the tracking script you need to change two environment variables:

- SECURITY_HEADER_SCRIPT_SRC_ALLOW to modify the
  header [`CSP: srcript-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src)
- SECURITY_HEADER_CSP_CONNECT_SRC to modify the
  header [`CSP: connect-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src)

### Track view changes

Add your custom tracking code to Settings > Custom JS

As Lychee uses history based routing you need to track page changes on the `navigate event` like this:

```javascript
navigation.addEventListener("navigate", function (e) {
    // track page logic goes here
});
```

---

## File: src/content/docs/docs/administration/honeypot.md

---
title: "Honeypot & Fail2Ban"
sidebar:
  order: 5
---

If you have had a look at your logs, you will notice that you get quite a few requests to pages that do not exists such as 
`wp-admin.php` _etc._ Those are automatic queries done by vulnerability crawlers.
This kind of trafic does not provide any value to the user and does not concern us.
For this reason, Lychee provides an honeypot.

If a user/robot/script queries one of the selected urls (honey) used by those vulnerability scanners, it will get a 418 response code.
This response is logged in Nginx/Apache, and we use [fail2ban](https://www.fail2ban.org/) to get rid of subsequent requests. 

## What is Fail2ban?

Fail2ban is a small service that can be run on your server to dynamically block clients before a request is executed.
It uses firewall rules to do so. After a certain amount of time (e.g. 1 day), the blocking rule is removed.

:::note
It will also ban you from your own website if you do a bad request.
:::


## Setting up Fail2Ban.

:::tip
**Fail2ban is not provided with Lychee**. It is an additional software that you will need to [install it yourself](https://www.fail2ban.org/wiki/index.php/Downloads).
:::


### Setup the Filter

Create `/etc/fail2ban/filter.d/filter-honeypot.conf` with:

```ini
[Definition]
failregex = ^<HOST>.*"(GET|POST).*" (418) .*$
ignoreregex =
```

This defines which regex to use to filter the log files.
From this regex, we retrieve the host if the response matches code 418 which is returned by Lychee when the honey is touched.

### Setup the Jail

Then we need to create the jail in:
`/etc/fail2ban/jail.d/honeypot.conf`

If you are using apache then the following will work:
```ini
[apache-honeypot]
enabled = true
filter = filter-honeypot
port = http,https
logpath = /var/log/apache2/access.log
maxretry = 1
```
In the case of Nginx:
```ini
[apache-honeypot]
enabled = true
filter = filter-honeypot
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 1
```

`maxretry` is set to 1 because we do not need to second guess those errors.
Fail2ban is also used to ban ssh attempts after multiple failures, in such case a higher number of retry is need.
As we interact with a honeypot, any behaviour touching it is therefore malicious, there are no false positive in our case
and we do not give the benefit of the doubt.

---

## File: src/content/docs/docs/administration/update.md

---
title: "Updating Lychee"
sidebar:
  order: 1
---

The way you can update Lychee depends on how you did your installation:

* If you used Docker, go to [Using Docker compose](#using-docker-compose)
* If you used the release channel (downloading a zip file), go to [Update manually](#update-manually)
* If you installed via `git clone`, go to [Update using Git](#update-using-git)

- Your system must comply with the latest [system requirements](/docs/getting-started/installation/) of the latest version of Lychee.
- Ensure that you are using the version 4 of Lychee. Updates from older version of Lychee are more complex.

Don't forget to take a look at the [Changelog](/docs/getting-started/releases/) to see what's new and to check for any actions that may be required.

## Using Docker compose

There are two cases. Either you are pinned to a release tag, _e.g._ v5.1.2 or you are using a rolling update tag.

:::tip
When using docker, a version rollback is difficult to apply: it requires to bash into the container to run the required migrate commands on the new version before dropping the tag to the previous value.
:::


#### With Rolling update tag

This procedure is for those following one of those tags:

- `latest` &mdash; the last official release
- `edge` or `master` &mdash; the last build from the `master` branch

Simply run the following.
```bash
docker compose down
docker compose pull
docker compose up -d
```

The database migrations will be applied automatically.

#### With version tag.

First edit your `docker-compose.yml` to point to the version you would like to migrate to.

```diff
-    image: lycheeorg/lychee:v5.1.0
+    image: lycheeorg/lychee:v5.1.2
```

Save and run the following.
```bash
docker compose down
docker compose pull
docker compose up -d
```

The migration will be applied automatically and you should be running the requested tagged version.

## Update manually

This update will be the one you have to use if you are following the Release channel.

1. Download the [newest Version](https://github.com/LycheeOrg/Lychee/releases)
2. Replace all existing files, excluding `public/uploads/`, `public/dist/user.css` and `.env` (and `database/database.sqlite` if using **SQLite** as your database)
3. Go into the `Diagnostic` page (this will mostly happen automatically) and click on the `Apply migration` button.

Note: if using `rsync` to upload stuff to the web server, something like this might be used: 

```bash
rsync -vrtz --delete --chmod=Du=rwx,Dg=rx,Do=rx,Fu=rw,Fg=r,Fo=r --exclude=public/uploads/ --exclude=public/dist/user.css --exclude=.env --exclude=database/database.sqlite Lychee/ user@web.server.web:~/www/your_path_to/lychee/
```

## Update using Git

Fully updating Lychee with `git` is the easiest way:

```bash
# download the lastst files
git pull
# update composer dependencies
composer install --no-dev
# apply the database migration
php artisan migrate
# update Node.js dependencies
npm install
# generate frontend assets
npm run build
```

If you have the `post-merge` hook set up the following is enough:
```bash
git pull
```

---

## File: src/content/docs/docs/administration/upgrade.md

---
title: "Upgrading to version 7"
sidebar:
  order: 2
---

:::note
**Critical Breaking Changes**: Version 7 introduces significant architectural changes to the Docker setup. **You must update your docker-compose configuration** when upgrading from v6 to v7. Simply changing the image tag will not work.
:::


### Major Changes in Version 7

Version 7 marks a fundamental shift in Lychee's Docker architecture:

1. **FrankenPHP replaces nginx + PHP-FPM**: The application now runs on FrankenPHP with Laravel Octane for dramatically improved performance
2. **Simplified volume mounts**: Individual directory mounts have been consolidated into cleaner volume structures
3. **Optional worker service**: Background job processing can now be scaled independently
4. **New environment variables**: Worker mode configuration requires specific settings

Do note that this change also has consequences in the way Lychee reads your `.env`
file. Updating values in the `.env` file will now require a container restart to take effect.

### Port change

Version 7 uses FrankenPHP which listens on port `8000` instead of the previous `80` used by nginx. Make sure to update your port mappings accordingly in your `docker-compose.yml`.

### Volume Mount Changes

In order to avoid you running into issues while booting version 7, we are blocking the startup of the container if the old volume structure is detected.

#### Version 6 Volume Structure (OLD)
```yaml
volumes:
  - ./lychee/conf:/conf
  - ./lychee/uploads:/uploads
  - ./lychee/sym:/sym
  - ./lychee/logs:/logs
  - ./lychee/tmp:/lychee-tmp
```

#### Version 7 Volume Structure (NEW)
```yaml
volumes:
  - ./lychee/uploads:/app/public/uploads
  - ./lychee/logs:/app/storage/logs
  - ./lychee/tmp:/app/storage/tmp  # so that uploads are not filling up the memory of the container
  - ./lychee/storage/app:/app/storage/app # OPTIONAL: for persistent storage of app data
  - ./conf/user.css:/app/public/dist/user.css # OPTIONAL
  - ./conf/custom.js:/app/public/dist/custom.js # OPTIONAL
```

:::note
Notice the key changes: uploads are now at `/app/public/uploads`, logs at `/app/storage/logs`, tmp at `/app/storage/tmp`, and the `.env` file.
:::


The `/sym` volume has been removed as Lychee no longer uses symbolic links for storage. This was a security feature that originated from version 4, but is no longer necessary as the functionality has been removed.

**Important:** With multiple volumes under `/app/storage`, you may think you could simplify the configuration by specifying one single volume for `/app/storage` instead. This is incorrect. Doing so will make the app exit with the error "The `/app/bootstrap/cache` directory must be present and writable."

### Service Architecture Changes

Version 7 introduces a multi-service architecture with an optional worker service for background job processing.

#### Basic Setup (Single Service)

For basic installations without background workers:

```yaml
services:
  lychee_api:
    image: ghcr.io/lycheeorg/lychee:edge
    container_name: lychee
    ports:
      - "${APP_PORT:-8000}:8000"
    volumes:
      - ./lychee/uploads:/app/public/uploads
      - ./lychee/logs:/app/storage/logs
      - ./lychee/tmp:/app/storage/tmp
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=lychee_db
      - DB_PORT=3306
      - DB_DATABASE=lychee
      - DB_USERNAME=lychee
      - DB_PASSWORD=your_password
      # ... other environment variables
    depends_on:
      - lychee_db
    networks:
      - lychee
```

#### Advanced Setup (With Workers)

For better performance with background job processing:

```yaml
services:
  lychee_api:
    image: ghcr.io/lycheeorg/lychee:edge
    container_name: lychee
    ports:
      - "${APP_PORT:-8000}:8000"
    volumes:
      - ./lychee/uploads:/app/public/uploads
      - ./lychee/logs:/app/storage/logs
      - ./lychee/tmp:/app/storage/tmp
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=lychee_db
      - QUEUE_CONNECTION=database  # CRITICAL for worker mode
      # ... other environment variables
    depends_on:
      - lychee_db
    networks:
      - lychee

  lychee_worker:
    image: ghcr.io/lycheeorg/lychee:edge
    container_name: lychee_worker
    volumes:
      - ./lychee/uploads:/app/public/uploads
      - ./lychee/logs:/app/storage/logs
      - ./lychee/tmp:/app/storage/tmp
    environment:
      - LYCHEE_MODE=worker  # CRITICAL: Tells container to run in worker mode
      - DB_CONNECTION=mysql
      - DB_HOST=lychee_db
      - DB_PORT=3306
      - DB_DATABASE=lychee
      - DB_USERNAME=lychee
      - DB_PASSWORD=your_password
      - QUEUE_CONNECTION=database  # CRITICAL: Must match API service
      # ... other environment variables
    depends_on:
      - lychee_db
      - lychee_api
    networks:
      - lychee
```

:::note
**Critical**: When using worker services, you **must** set `QUEUE_CONNECTION=database` (or `redis` if using Redis) in **both** the API and worker services. Without this, jobs will not be processed properly.
:::


### Worker Service Benefits

The worker service provides several advantages:

- **Parallel Processing**: Scale workers independently by running multiple instances
- **Better Performance**: Offload heavy tasks (photo processing, imports) from the main API
- **Resource Management**: Allocate different resources to API and workers

#### Scaling Workers

You can run multiple worker instances for parallel processing:

```yaml
lychee_worker:
  image: ghcr.io/lycheeorg/lychee:edge
  deploy:
    replicas: 3  # Run 3 worker instances
  # ... rest of configuration
```

Or manually with different container names:

```yaml
lychee_worker_1:
  image: ghcr.io/lycheeorg/lychee:edge
  container_name: lychee_worker_1
  environment:
    - LYCHEE_MODE=worker
    # ... rest of configuration

lychee_worker_2:
  image: ghcr.io/lycheeorg/lychee:edge
  container_name: lychee_worker_2
  environment:
    - LYCHEE_MODE=worker
    # ... rest of configuration
```

### Migration Steps

Follow these steps to migrate from v6 to v7:

#### 1. **Backup Everything**

```bash
# Backup your database
docker exec lychee_db mysqldump -u lychee -p lychee > lychee_backup.sql

# Backup your uploads and configuration
cp -r ./lychee ./lychee_backup
```

#### 2. **Stop Your Current v6 Services**

```bash
docker-compose down
```

#### 3. **Update Your docker-compose.yml**

Replace your v6 docker-compose.yml with the v7 configuration. You can find the complete example at: [https://github.com/LycheeOrg/Lychee/blob/master/docker-compose.yaml](https://github.com/LycheeOrg/Lychee/blob/master/docker-compose.yaml)

#### 4. **Reorganize Your Volume Data** (if needed)

If your current directory structure doesn't match the new volume mounts, reorganize:
  
```bash
# The uploads directory structure should remain compatible
# Ensure your uploads are in ./lychee/uploads/
```

#### 5. **Update Environment Variables**

Key changes to your environment configuration:
- If using workers: Add `QUEUE_CONNECTION=database` or `QUEUE_CONNECTION=redis`
- If using workers: Add `LYCHEE_MODE=worker` to worker service only
- Review other environment variables for any deprecated options

#### 6. **Start v7 Services**
```bash
docker-compose up -d
```

#### 7. **Verify Installation**

Check that all services are running:
```bash
docker-compose ps
```

Check logs for errors:
```bash
docker-compose logs -f lychee
```

#### 8. **Fix the thumbnails**

You will notice that after the upgrade, thumbnails are missing. You can regenerate them by running:
```bash
docker exec -it lychee php artisan lychee:recompute-album-sizes
docker exec -it lychee php artisan lychee:recompute-album-stats
```

or by logging into the web interface and going to Settings &rArr; Maintenance &rArr; Album Precomputed Fields.


### Troubleshooting

**Container keeps restarting**

- Verify that you have APP_KEY properly set in your `.env` or `docker-compose.yml` if that is not the case you can run `echo "APP_KEY=base64:$(openssl rand -base64 32)"` to create a new one.

**Workers not processing jobs**
- Verify `QUEUE_CONNECTION=database` is set in both API and worker services
- Verify `LYCHEE_MODE=worker` is set in worker service
- Check worker logs: `docker-compose logs -f lychee_worker`

**Upload issues**
- Verify volume mounts point to the correct paths
- Check file permissions on host directories
- Ensure uploads directory: `./lychee/uploads` exists and is writable

**Performance issues**
- Consider adding worker services for background processing
- Check FrankenPHP is running (should see FrankenPHP in logs, not nginx)
- Verify `QUEUE_CONNECTION` is set for async job processing

**Database connection errors**
- Ensure database service name matches `DB_HOST` value
- Verify database credentials are correct
- Check database service is healthy: `docker-compose ps lychee_db`

For more help, visit our [GitHub Discussions](https://github.com/LycheeOrg/Lychee/discussions) or [Discord server](https://discord.gg/y4aUbnF).


## Migrating from Traditional Installation to Docker Compose

This guide helps you migrate an existing Lychee installation from a traditional setup (e.g., `/var/www/html/Lychee`) to a Docker Compose deployment.

### Prerequisites

Before starting the migration, make sure you have docker compose installed. You can follow the instructions [here](https://docs.docker.com/compose/install/).

### Migration Steps

#### 1. **Backup Your Current Installation**

:::note
**Critical**: Always create backups before migrating. This ensures you can restore your installation if anything goes wrong.
:::


```bash
# Backup the database
mysqldump -u your_db_user -p your_db_name > ~/lychee_db_backup.sql

# Backup the entire Lychee directory
cp -r /var/www/html/Lychee ~/lychee_backup

# Backup your .env file specifically
cp /var/www/html/Lychee/.env ~/lychee_env_backup
```

#### 2. **Create Docker Compose Directory Structure**

Create a directory for your Docker Compose setup:

```bash
mkdir -p ~/lychee-docker
cd ~/lychee-docker

# Create directories for volume mounts
mkdir -p lychee/uploads
mkdir -p lychee/storage/app
mkdir -p lychee/logs
mkdir -p lychee/tmp
mkdir -p lychee/conf
```

#### 3. **Copy Your Data**

Move your existing uploads and configuration:

```bash
# Copy uploads (this may take time depending on your photo library size)
sudo cp -r /var/www/html/Lychee/public/uploads/* ~/lychee-docker/lychee/uploads/

# Copy your .env configuration
sudo cp /var/www/html/Lychee/.env ~/lychee-docker/lychee/conf/.env

# If you have custom CSS or JavaScript
sudo cp /var/www/html/Lychee/public/dist/user.css ~/lychee-docker/conf/user.css 2>/dev/null || true
sudo cp /var/www/html/Lychee/public/dist/custom.js ~/lychee-docker/conf/custom.js 2>/dev/null || true

# Set appropriate permissions
sudo chown -R $USER:$USER ~/lychee-docker/lychee
chmod -R 755 ~/lychee-docker/lychee
```

#### 4. **Create docker-compose.yml**

Create a `docker-compose.yml` file in `~/lychee-docker`:

```yaml
services:
  lychee_db:
    image: mariadb:11
    container_name: lychee_db
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=lychee
      - MYSQL_USER=lychee
      - MYSQL_PASSWORD=lychee_password
    volumes:
      - ./lychee/lychee_db:/var/lib/mysql
    networks:
      - lychee
    restart: unless-stopped

  lychee_api:
    image: ghcr.io/lycheeorg/lychee:latest
    container_name: lychee
    ports:
      - "8000:8000"  # Change the first part XXXX:8000 this to your preferred port
    env_file:
      - ./lychee/conf/.env
    volumes:
      - ./lychee/uploads:/app/public/uploads
      - ./lychee/storage/app:/app/storage/app
      - ./lychee/logs:/app/storage/logs
      - ./lychee/tmp:/app/storage/tmp
      - ./lychee/conf/.env:/app/.env
      # Optional: Uncomment if you have custom CSS/JS
      # - ./conf/user.css:/app/public/dist/user.css
      # - ./conf/custom.js:/app/public/dist/custom.js
    environment:
      - APP_KEY=base64:YOUR_APP_KEY_HERE # Generate it with `openssl rand -base64 32`
      - DB_CONNECTION=mysql
      - DB_HOST=lychee_db
      - DB_PORT=3306
      - DB_DATABASE=lychee
      - DB_USERNAME=lychee
      - DB_PASSWORD=lychee_password
    depends_on:
      - lychee_db
    networks:
      - lychee
    restart: unless-stopped

networks:
  lychee:
```

#### 6. **Update Your .env File**

Edit `~/lychee-docker/lychee/conf/.env` to update database connection settings:

```bash
# Update these values to match your docker-compose.yml
DB_CONNECTION=mysql
DB_HOST=lychee_db
DB_PORT=3306
DB_DATABASE=lychee
DB_USERNAME=lychee
DB_PASSWORD=lychee_password

# Set the application URL
APP_URL=http://your-domain.com:8000  # Update with your domain/IP
```

#### 7. **Import Your Database**

Start the database container and import your data:

```bash
cd ~/lychee-docker

# Start only the database service
docker-compose up -d lychee_db

# Wait for the database to be ready (about 10-20 seconds)
sleep 20

# Import your database backup
docker exec -i lychee_db mysql -u lychee -plychee_password lychee < ~/lychee_db_backup.sql
```

#### 8. **Start Lychee**

```bash
# Start all services
docker-compose up -d

# Check logs to ensure everything started correctly
docker-compose logs -f lychee
```

Press `Ctrl+C` to exit log viewing.

#### 9. **Configure Reverse Proxy (Optional)**

If you're using a reverse proxy (recommended for production), configure it to forward to the Docker container.

**Nginx Example:**

```nginx
server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://localhost:8000;
        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;

        # For large photo uploads
        client_max_body_size 100M;
    }
}
```

**Apache Example:**

```apache
<VirtualHost *:80>
    ServerName your-domain.com

    ProxyPreserveHost On
    ProxyPass / http://localhost:8000/
    ProxyPassReverse / http://localhost:8000/

    # For large photo uploads
    LimitRequestBody 104857600
</VirtualHost>
```

Enable required modules and restart:
```bash
# For Apache
sudo a2enmod proxy proxy_http
sudo systemctl restart apache2

# For Nginx
sudo systemctl restart nginx
```

#### 10. **Verify Migration**

1. Access Lychee at `http://your-domain.com` (or `http://your-domain.com:8000` if not using reverse proxy)
2. Log in with your existing credentials
3. Verify your photos and albums are displayed correctly
4. Check Settings &rArr; Diagnostics to ensure everything is working

#### 11. **Optional: Add Worker Service**

For better performance with large photo libraries, consider adding a worker service:

```yaml
  lychee_worker:
    image: ghcr.io/lycheeorg/lychee:latest
    container_name: lychee_worker
    volumes:
      - ./lychee/uploads:/app/public/uploads
      - ./lychee/storage/app:/app/storage/app
      - ./lychee/logs:/app/storage/logs
      - ./lychee/tmp:/app/storage/tmp
      - ./lychee/conf/.env:/app/.env:ro
    environment:
      - LYCHEE_MODE=worker
      - DB_CONNECTION=mysql
      - DB_HOST=lychee_db
      - DB_PORT=3306
      - DB_DATABASE=lychee
      - DB_USERNAME=lychee
      - DB_PASSWORD=lychee_password
      - QUEUE_CONNECTION=database
    depends_on:
      - lychee_db
      - lychee_api
    networks:
      - lychee
    restart: unless-stopped
```

Also add `QUEUE_CONNECTION=database` to the `lychee_api` service environment variables, then restart:

```bash
docker-compose up -d
```

### Post-Migration Cleanup

After confirming everything works correctly:

```bash
# Disable the old web server from starting on boot
sudo systemctl disable apache2  # or nginx

# You can remove the old installation (keep the backup!)
# sudo rm -rf /var/www/html/Lychee  # Only after thorough testing!
```

### Troubleshooting

**Cannot access Lychee**
- Check if containers are running: `docker ps`
- Check logs: `docker logs lychee`
- Verify port 8000 is not blocked by firewall
- If using reverse proxy, check proxy configuration

**Photos not showing**
- Verify uploads were copied correctly: `ls -la ~/lychee-docker/lychee/uploads/`
- Check volume mount permissions
- Verify file paths in database match new structure

**Database connection errors**
- Confirm database container is running: `docker ps lychee_db`
- Verify credentials in `.env` match `docker-compose.yml`
- Check database logs: `docker logs lychee_db`

**Permission errors**
- Fix ownership: `sudo chown -R 33:33 ~/lychee-docker/lychee/` (33 is www-data UID/GID but can also be 82 for alpine)
- Or make directories writable: `chmod -R 777 ~/lychee-docker/lychee/` (less secure)

For additional help, visit our [GitHub Discussions](https://github.com/LycheeOrg/Lychee/discussions) or [Discord server](https://discord.gg/y4aUbnF).


## Upgrading from Lychee v3 to v4

### Checking requirements

Check that the server satisfies the [requirements](/docs/getting-started/manual-installation/#web-server-configuration). In particular pay attention the PHP extensions.
You can display installed PHP extensions using `phpinfo()`.

### Preparing the files

Assuming the following tree:
```
/var/
  |- www/
         |- html/
              |- Lychee/
              |- <you are here>
```

Rename Lychee into Lychee-v3:
```bash
mv Lychee Lychee-v3
```

Install Lychee files by either uploading the content of the released zip or cloning the repository:
```bash
git clone https://github.com/LycheeOrg/Lychee Lychee
```

Move the pictures from the version 3 to the newly created installation:
```bash
mv Lychee-v3/uploads/big/* Lychee/public/uploads/big/
mv Lychee-v3/uploads/medium/* Lychee/public/uploads/medium/
mv Lychee-v3/uploads/small/* Lychee/public/uploads/small/
mv Lychee-v3/uploads/thumb/* Lychee/public/uploads/thumb/
```

### Preparing the server

:::note
The big difference between Lychee version 3 and Lychee version 4 is the served directory, i.e. where you web server needs to point to.
:::


- In the version 3, this was the root `.` of Lychee.
- In the version 4, this is the `public` directory inside Lychee.

#### Using Apache

**Make sure you have the module rewrite available and enabled: `a2enmod rewrite`**.

Modify your `/etc/apache2/apache2.conf` to allow `.htaccess` to set up the rewrite rules:
```apache
<Directory /var/www/html/Lychee>
	Options Indexes FollowSymLinks
	AllowOverride All
	Require all granted
</Directory>
```

Modify or create `example.com.conf` in `/etc/apache2/sites-available/` to point out the served directory:
```apache
<VirtualHost *:80>
	ServerName example.com

	DocumentRoot /var/www/html/Lychee/public

	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
```

Enable the site:
```bash
a2ensite `example.com.conf`
```

Restart apache2:
```bash
systemctl restart apache2
```

#### Using Nginx

If you are using Nginx, an example configuration can be found [here](/docs/getting-started/manual-installation/#web-server-configuration).

#### Run the migrations

Make sure you have `DB_OLD_LYCHEE_PREFIX` set in your `.env` to correspond the table prefix of your old database, then run `php artisan migrate`.

---

## File: src/content/docs/docs/faq/general.md

---
title: "General FAQ"
sidebar:
  order: 1
---

### Which browsers are supported?

Lychee should work with any modern, standards-compliant web browser (note that having JavaScript enabled is a hard dependency). Lychee does take advantage of several HTML5 features, such as the `<video>` tag, responsive image sizing using `srcset`, or link data prefetching. In particular, Lychee supports the latest versions of Google Chrome, Apple's Safari, Mozilla Firefox, Opera, and Microsoft Internet Explorer.

If you experience any issues with Lychee and wish to report it, make sure to specify the browser you're using and the version of it.

### Which file formats are supported?

Lychee supports major image formats, and since version 3.2.1 some video formats as well. Specifically, `*.jpg`, `*.jpeg`, `*.png`, `*.gif`, `*.webp`, `*.ogv`, `*.mp4`, `*.mpg`, `*.webm`, `*.mov`, `*.m4v`, `*.avi` and `*.wmv` are accepted.

If you're uploading video files, make sure to increase your upload limits in `php.ini`.  See the [Installation](/docs/getting-started/installation/) section for more information.

### How to mass import photos?

Lychee supports mass import of photos via the command line. You can use the following command to import photos from a given path:

```bash
php artisan lychee:sync /path/to/import
```

This command will import all photos from the specified path, creating albums for each folder and subfolder. You can also specify an album ID to import photos into a specific album:

```bash
php artisan lychee:sync /path/to/import --album_id="album ID"
```

If you want to do this within docker, then we recommend you mount the volume containing the photos to be imported to a temporary location in the container, for example `/tmp/import`, and then run the command:

```bash
docker exec -it <container_name> php artisan lychee:sync /tmp/import
```

This will create an album `import` in which all the photos will be imported. You can then move the photos to the desired albums using the web interface. Note that this import is recursive, so any subfolders will be imported as subalbums.

Note that `lychee:sync` has a few options:

- `--album_id[=ALBUM_ID]`: Album ID to import to.
- `--owner_id[=OWNER_ID]`: Owner ID of imported photos [default: "1"]
- `--resync_metadata[=RESYNC_METADATA]`: Re-sync metadata of existing files [default: "1"]
- `--delete_imported[=DELETE_IMPORTED]`: Delete the original files [default: "1"]
- `--import_via_symlink[=IMPORT_VIA_SYMLINK]`: Import photos via symlink instead of copying the files [default: "0"]
- `--skip_duplicates[=SKIP_DUPLICATES]`: Skip photos and albums if they already exist in the gallery [default: "0"]
- `--delete_missing_photos[=DELETE_MISSING_PHOTOS]`: Delete photos in the album that are not present in the synced directory [default: "0"]
- `--delete_missing_albums[=DELETE_MISSING_ALBUMS]`: Delete albums in the parent album that are not present in the synced directory [default: "0"]
- `--dry_run[=DRY_RUN]`: Run the delete photos process but do not make any changes [default: "1"]

We strongly recommend to specify those or at least run `php artisan lychee:sync --help` to understand the default values of those options as they are specified with your configuration. The `--delete_xxx` can have disastrous consequences if you are not careful.


### What is new?

Take a look at the [Release Notes](/docs/getting-started/releases/) to see what's new.

### How can I easily contact the LycheeOrg organization?

There is a discord associated with the project, feel free to join us there: https://discord.gg/JMPvuRQcTf

### How can I set thumbnails for my albums?

Thumbnails are selected automatically from the photos inside the album (and any subalbums) based on the photo sorting order specified in the [Settings](/docs/getting-started/settings/#sorting_photos_col). _Precedence is given to starred photos_. In practical terms, if only one photo inside an album is starred, that photo is guaranteed to be the top thumbnail.

### Is it possible to create folders inside another folder? If so, how to do that?

Either press `n` (as **N**ew) or use the add menu.



### What headers need to be used to authenticate with the custom `api_key` in the settings?

In order to bypass the CSRF protection, you can set up the `api_key` setting to a secret value and send that value over in the `Authorization` header.  
Note that `api_key` only disables the CSRF protection, you still need to authenticate to the server.

In order to authenticate, use [Session::login](https://demo.lycheeorg.dev/docs/api) and pass the returned `lychee_session` cookie to all subsequent requests.

The related code is available [here](https://github.com/LycheeOrg/Lychee/blob/master/app/Http/Middleware/VerifyCsrfToken.php#L55)

### Can I use my existing folder structure?

Not at this time. Lychee currently uses its own folder structure and database. Please upload or import all your photos to use them.

### Can I upload videos?

Yes, but you may need to change this property to a bigger value:
```ini
upload_max_filesize = 100M
```

### Is it possible to create multiple users?

Yes. Just go to the `Users` menu.

### Does Lychee use ImageMagick?

Yes. Lychee uses ImageMagick when available.

### How to change the title of the site?

Go to the advanced Settings menu and change the value of `site_title`.

### How to reset username and password?

Via SSH, use the command `php artisan lychee:reset_admin`.

### How to hide smart albums?

Go into your _Settings_ then _More_, find `SA_enabled` in the _"Smart Albums"_ section and set it to `0`.

### How to disable the 'zoom' animation while browsing pictures?

Add the following custom CSS to your `user.css` or via the settings menu:
```css
#imageview #image {
  transition: none !important;
  animation-name: none !important;
  animation-duration: 0 !important;
}
```

### How to use a custom font on the landing page?

In order to use a specific font on the landing page which does not rely on some font provided by the client-side browser, you have two options:

1. self-host the necessary set of font files as part of your Lychee installation
2. use an external font-hosting service such as Google Fonts

Both options require to tweak the custom `user.css` via the settings menu.

#### Option 1: Self-hosted font files

Let's assume that your favorite font is called `My Font`. You need a set of the font files in WOFF2 format with one font file per used font weight and style. The landing page uses 5 different font weights (200 = thin, 300 = light, 400 = normal, 500 = thick, 700 = bold) all in normal (i.e. upright) style.

The necessary CSS consists of two parts: firstly, the font face and its different weights are declared via the `@font-face`-directive; secondly, the declared font needs to be used by the styles for the page. A template CSS code for `user.css` looks like this.

```css
@font-face {
    font-family: "My Font";
    font-style: normal;
    font-weight: 200;
    src: local(""), url("<font URL for weight 200>") format("woff2");
}
@font-face {
    font-family: "My Font";
    font-style: normal;
    font-weight: 300;
    src: local(""), url("<font URL for weight 300>") format("woff2");
}
@font-face {
    font-family: "My Font";
    font-style: normal;
    font-weight: 400;
    src: local(""), url("<font URL for weight 400>") format("woff2");
}
@font-face {
    font-family: "My Font";
    font-style: normal;
    font-weight: 500;
    src: local(""), url("<font URL for weight 500>") format("woff2");
}
@font-face {
    font-family: "My Font";
    font-style: normal;
    font-weight: 700;
    src: local(""), url("<font URL for weight 700>") format("woff2");
}

#logo h1 span,
#logo h1,
#intro,
.menu .menu-item {
    font-family: "My Font", sans-serif !important;
}
```

#### Option 2: Using an external font-hosting service

If you want to use an external font-hosting service, replace the `@font-face` blocks of the template above with an `@import`-directive which points to the externally hosted font. The name of the font-family must be replaced by the font name as documented by the external font-service.
For example, let's assume you want use Google's Nunito font. Then the CSS has to look like

```css
@import url("https://fonts.googleapis.com/css?family=Nunito:200,300,400,500,700");
#logo h1 span,
#logo h1,
#intro,
.menu .menu-item {
    font-family: "Nunito", sans-serif !important;
}
```

You also have to change the CSP (Content Security Policy) to allow loading fonts from the external server. Therefore, add the following to the `csp` array in `config/secure-headers.php` (for example, below the `sandbox` line, one of the last lines in this file):

```php
		'style-src' => [
			'allow' => [
				'https://fonts.googleapis.com',
			],
			'nonces' => [],
			'self' => true,
			'unsafe-inline' => true,
			'report-sample' => true,
			'add-generated-nonce' => false,
		],
		'font-src' => [
			'allow' => [
				'https://fonts.gstatic.com',
			],
			'self' => true,
		],
```

Change the URLs `https://fonts.googleapis.com` and `https://fonts.gstatic.com` to match your provider's URLs.

### How to add custom scripts?

Edit the `custom.js` file in `/path/to/lychee/public/dist/` or use the _"Personalize JS"_ area in _Settings_.

### How is the upload folder protected?

There are now two possibilities:

- Use signed URLs to access the photos and avoid hot-linking, via the [`temporary_image_link_enabled`](/docs/getting-started/settings/#temporary_image_link_enabled) setting.
- Use AES encrypted URLs, via the [`secure_image_link_enabled`](/docs/getting-started/settings/#secure_image_link_enabled) setting (only available for SE users).

### My login is timing out after two hours, how can this be changed?

You can edit your `.env` and modify the `SESSION_LIFETIME=120` part (in minutes).

### How can I see the correct client IP address when running Lychee behind Cloudflare?

Please see [https://github.com/monicahq/laravel-cloudflare](https://github.com/monicahq/laravel-cloudflare). The Lychee file that needs changing can be found [here](https://github.com/LycheeOrg/Lychee/blob/master/config/trustedproxy.php).

### Can I set up Lychee to watch a folder for new images and automatically add them to albums?

Lychee can import photos from the command line using `php artisan lychee:sync /path/to/import`. Folders in this path will be converted to albums, and subfolders as subalbums using `php artisan lychee:sync /path/to/import --album_id="album ID"`.



This command can be scheduled using `cron` or a systemd timer.
```bash
man crontab
man systemd.timer
```


### Special right-click menu doesn't appear for new users?

For example:  


No this is normal. This user does not have the ownership of that Album, so the right click is not available. You can see _sharing_ as a _read_ permission.

---

## File: src/content/docs/docs/faq/installation.md

---
title: "Installation FAQ"
sidebar:
  order: 2
---

### How can I install Lychee without SSH access?

1. Download the latest release
2. Extract and upload the folder via FTP
3. access the website

If you are at the wrong address you will be told to go to the `public` folder.
Once open, you will be redirected to the install procedure. Completed you will be able to create an admin account and enjoy Lychee.

### How do I upgrade from Lychee v3 to Lychee v4?

The process is described [here](/docs/administration/upgrade/).

### How can I back up my installation?

To back up your Lychee installation you need to perform the following steps:

1. Create a copy of at least the following parts of the Lychee directory tree (e.g., `/var/www/html/Lychee`):
```
.env
public/dist/user.css
public/uploads/
```
2. Dump the Lychee database to a file. E.g., if you are using MySQL, run:
```bash
mysqldump -u user -ppassword --databases lychee_database > lychee_backup.sql
```
Replace `user`, `password`, and `lychee_database` by the values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE` from the `.env` file in the Lychee folder.

### How can I migrate my installation to a new host?

1. Back up your installation as described above
2. Download the latest release of Lychee.
3. Overwrite the files with your back up
4. Restore the database on the new host, e.g., for MySQL: `mysql -u user -ppassword < lychee_backup.sql`

### Can I host Lychee with a subpath with Nginx? Like `https://example.dev/lychee/`

Yes, here is a configuration to help you:

```nginx
location ^~ /lychee {
    alias /var/www/lychee/public;
    index index.php;
    try_files $uri $uri/ @lychee;
    location ~ \.php$ {
        if (!-e $request_filename) {
            rewrite ^/lychee/?(.*)$ /lychee/index.php?/$1 last;
            break;
        }
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $request_filename;
    }
}
location @lychee {
    rewrite /lychee/(.*)$ /lychee/index.php?/$1 last;
}
```

### Can I host Lychee with a subpath with Apache? Like `https://example.dev/lychee/`

Yes, the process is described [here](/docs/administration/advanced-setups/#hosting-lychee-in-a-subpath-with-apache).

### Do we really need writable `app/`?

From [#311](https://github.com/LycheeOrg/Lychee/issues/311)

Short answer: Lychee will work without a writable `app/` folder.

The minimum set of directories that need to be http-writable is as follows:
```
storage/
public/uploads/small
public/uploads/big
public/uploads/thumb
public/uploads/medium
public/uploads/import
public/dist
```

### Can I migrate from a 64-bit system to a 32-bit system?

Yes, but it's not trivial or recommended. After copying the database:
1. Download [this](https://github.com/LycheeOrg/Lychee/raw/54d00878949906c2efd4f6ddd9e79669637c58fb/database/migrations/2019_04_07_193345_fix_32bit.php) file to your `database/migrations/` folder.
2. Run  the SQL command `delete from migrations where migration='2019_04_07_193345_fix_32bit';` to make sure it will run.
3. Run `php artisan migrate`. This should run a one-off migration that was originally added to allow 32-bit systems to migrate from Lychee v3.

This will only work on top-level albums. Subalbums will require [manual intervention](https://github.com/LycheeOrg/Lychee/issues/406#issuecomment-571378073).

### How can I stop `artisan migrate` and `composer` from running after every `git pull`?

We've set composer to install git `pre-commit` and `post-merge` hooks by default.
- The pre-commit is to help developers by checking and fixing and code style problems before they hit our testing. If you aren't committing and changes, this will not be run.
- The post-merge ensures that your dependencies and database version are kept current. This can be disabled by creating a file `.NO_AUTO_COMPOSER_MIGRATE` in your Lychee root and deleting (if necessary) `.git/hooks/post-merge`. If you disable this script, you will need to run `composer` and `artisan migrate` manually.

---

## File: src/content/docs/docs/faq/troubleshooting.md

---
title: "Troubleshooting FAQ"
sidebar:
  order: 3
---

### Lychee is not working

If Lychee is not working properly, try to open `https://lychee.example.com/Diagnostics`. This script will display all errors it can find.

Another way to see this screen is to use the command: `php artisan lychee:diagnostics`

### When I update my .env with Lychee v7, the changes are not taken into account, what can I do?

Lychee v7 with FrankenPHP requires a container restart to take into account changes made to the `.env` file.

### My Docker container keeps restarting, what can I do?

If your Lychee Docker container is continuously restarting, the most common cause is a missing or improperly configured `APP_KEY`.

Verify that you have `APP_KEY` properly set in your `.env` file or `docker-compose.yml`. If the key is missing or invalid, you can generate a new one by running:

```bash
echo "APP_KEY=base64:$(openssl rand -base64 32)"
```

Copy the generated key and add it to your `.env` file or Docker Compose configuration, then restart the container.

### My workers are not processing jobs, what should I check?

If background job processing is not working in your Docker setup, verify the following settings:

1. Ensure `QUEUE_CONNECTION=database` is set in both the API and worker services
2. Verify `LYCHEE_MODE=worker` is set in the worker service configuration
3. Check the worker logs for any errors:

```bash
docker-compose logs -f lychee_worker
```

Review the logs for any error messages that might indicate configuration issues or connection problems.

### I'm having upload issues with Docker, what can I do?

Upload problems in Docker deployments are typically related to volume mounting or permissions:

1. Verify that your volume mounts point to the correct paths in your `docker-compose.yml`
2. Check that file permissions on host directories allow the container user to write
3. Ensure the uploads directory (typically `./lychee/uploads`) exists on the host and is writable

You can check directory permissions on your host system with:

```bash
ls -la ./lychee/
```

If needed, adjust permissions to allow the container to write to the uploads directory.

### I'm experiencing performance issues with Docker, what should I check?

If your Dockerized Lychee instance is running slowly, consider the following:

1. Add worker services for background processing of resource-intensive tasks
2. Verify FrankenPHP is running correctly (you should see FrankenPHP mentioned in the logs, not nginx)
3. Ensure `QUEUE_CONNECTION` is set to enable asynchronous job processing

You can check your container logs to verify FrankenPHP is active:

```bash
docker-compose logs lychee
```

### I just updated to 7.3+ and I get PHP Fatal error: Uncaught RuntimeException: A facade root has not been set.

This error is misleading. Version 7.3 added support for LDAP, which requires the `php-ldap` extension.
Verify that you have all the required PHP extensions installed.
### I'm getting database connection errors in Docker, what can I do?

Database connection issues in Docker are usually related to service configuration:

1. Ensure the database service name in your `docker-compose.yml` matches the `DB_HOST` value in your configuration
2. Verify database credentials (`DB_USERNAME`, `DB_PASSWORD`, `DB_DATABASE`) are correct and match between services
3. Check that the database service is healthy and running:

```bash
docker-compose ps lychee_db
```

If the database service shows as unhealthy or restarting, check its logs for more details:

```bash
docker-compose logs lychee_db
```

### Since upgrading to Lychee v7, all my thumbnails are missing, what can I do?

After upgrading to Lychee v7, if your thumbnails are missing, you may need to regenerate them. You can do this by running the following Artisan command inside your Lychee container:

```bash
docker exec -it lychee php artisan lychee:recompute-album-sizes
docker exec -it lychee php artisan lychee:recompute-album-stats
```

Or log into the web interface and go to Settings &rArr; Maintenance &rArr; Album Precomputed Fields.

### When I do X, I get an error API not found, what can I do?

Open the dev modules of your browser (usually by pressing `F12`) and open the Network tab.
Redo the action with Network tab open and look at the response of the last query (most likely a red line).
If it is an error 404 or 500, see below, otherwise look at our [issues](https://github.com/LycheeOrg/Lychee/issues) (including the closed ones).
If nothing helps, don't hesitate to open a new one.

### I have an error 404 and I'm using Apache, what can I do?

Verify that the rewrite rules of your server are correctly applied. See [here](/docs/administration/upgrade/#using-apache).

### I have an error 404 and I'm using Nginx, what can I do?

Verify that the rewrite rules of your server are correctly applied. See [here](/docs/administration/upgrade/#using-nginx).

### I have an error 404 and I'm using php -S, what can I do?

Instead of doing `php -S 127.0.0.1:8888` inside `Lychee/public` do `php artisan serve --port=8888` inside Lychee.
This will ensure that the paths and routes are correctly followed.

:::note
Note that the `serve` command should only be used for testing and debugging. It should not be used on a production server.
:::


### I have an error 500, it says 'Class "Mockery" not found', what can I do?

This is because your `APP_ENV` is set to `testing` and you are using production dependencies.
Lychee is trying to mock the requests for test purposes.

To resolve the issue, edit the `.env` file and set `APP_ENV` to something other than `testing`, for example `APP_ENV=production`.

### I have an error 500, what can I do?

Edit the `.env` file and set `APP_DEBUG` to `true`, this will allow the errors to be displayed with the trace.

### I have an errror 419 in the Network tab, what is going on?

Verify that your `SESSION_DRIVER` is not `array`. With `array`, the data is stored a non-persisting PHP array, it is only meant for testing !
More [here](https://laracasts.com/discuss/channels/general-discussion/how-does-cache-driver-array-work).  
Prefer `file` or `database` (but that last one require some [more configuration](https://laravel.com/docs/7.x/session#driver-prerequisites))

:::note
`php -S` web server was designed to aid application development. It may also be useful for testing purposes or for application demonstrations that are run in controlled environments. It is not intended to be a full-featured web server. It should not be used on a public network.
:::


### I can't upload (large) photos.

Issues may occur when trying to upload photos with large **file sizes** or large **resolutions**.

If you experience problems uploading photos with large **file sizes**, you might want to change the PHP parameters in `.htaccess` (if you are using the PHP Apache module) or in `.user.ini` (if you are using PHP with CGI or FastCGI).

> If you modify the `.user.ini` file, you may want to run `git update-index --assume-unchanged .user.ini` afterwards.

If possible, change these settings directly in your `php.ini`. We recommend to increase the values of the following properties:

```ini
max_execution_time = 200
post_max_size = 100M
upload_max_size = 100M
upload_max_filesize = 100M
memory_limit = 256M
```

Also check the settings for your web server (e.g. `client_max_body_size` in nginx) allow for large uploads.

If problems occur when uploading photos with large **resolutions**, the issue may lie with **ImageMagick Security Policy**.
Review and edit the options in `/etc/ImageMagick-6/policy.xml`.
Editing the `width`, `height` and `memory` options can help with enabling upload of images with larger resolutions.

For more information, see [here](https://legacy.imagemagick.org/script/security-policy.php) for ImageMagick 6 and [here](https://imagemagick.org/script/security-policy.php) for ImageMagick 7.

### What does _Upstream sent too big header_ error message mean?

This error may be seen from your browser's console if you're trying to debug something with Lychee. If using `nginx`, try adding the following to Lychee's config and reload nginx's service:
```nginx
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
```

### Why don't my videos have thumbnails?

You will need ffmpeg installed on your server, and to have installed php-ffmpeg using composer as detailed in the [Installation Guide](/docs/getting-started/installation/).

Once this is taken care of, check that the
[`has_ffmpeg` setting](/docs/getting-started/settings/) is set to `1`.  Lychee can reset it
to `0` if it can't find the binaries the first time it tries.  The Lychee
log will contain a `Failed to extract snapshot: bad config` notice if that
is the case.

If that doesn't help, check if metadata is extracted correctly from the
video files.  While viewing a video file in Lychee, simply open the info
sidebar (_i_) and check if the resolution, duration, and frame rate are
reported correctly.  If they are not, you may need to let the metadata
extractor know the location of your `ffprobe` binary.  The Lychee log will
contain a `Given path () to the ffprobe binary is invalid` error if this is
the issue.

Edit the file
`Lychee/vendor/lychee-org/php-exif/lib/PHPExif/Adapter/FFprobe.php`,
replacing the line:
```php
protected $toolPath;
```
(here's a
[sample location](https://github.com/LycheeOrg/php-exif/blob/1ea3468d4ea6a5cf0ea6c748a3a2376de38bbbfd/lib/PHPExif/Adapter/FFprobe.php#L36))
with:
```php
protected $toolPath = '/usr/bin/ffprobe';
```
using your correct binary location. If unsure, you can try running
`which ffprobe` on the server.

This will likely need to be repeated for the video frame extraction code;
otherwise, you will see in the Lychee log an `Unable to load FFProbe` error.
To fix that, in Lychee versions prior to 4.2.0, edit the file
`Lychee/app/ModelFunctions/PhotoFunctions.php`, replacing the line:
```php
$ffmpeg = FFMpeg\FFMpeg::create();
```
(here's the
[location for version 4.1.0](https://github.com/LycheeOrg/Lychee/blob/v4.1.0/app/ModelFunctions/PhotoFunctions.php#L566))
with:
```php
$ffmpeg = FFMpeg\FFMpeg::create(array(
        'ffmpeg.binaries'  => '/usr/bin/ffmpeg',
        'ffprobe.binaries' => '/usr/bin/ffprobe',
));
```
again, using your correct binary locations.

Starting with version 4.2.0, the file to edit is instead `Lychee/app/Actions/Photo/Extensions/VideoEditing.php`; replace the line:
```php
$ffmpeg = FFMpeg::create();
```
(here's a
[sample location](https://github.com/LycheeOrg/Lychee/blob/23731e104737175f51a9acef199bf7d8829e5d5c/app/Actions/Photo/Extensions/VideoEditing.php#L48))
with:
```php
$ffmpeg = FFMpeg::create(array(
        'ffmpeg.binaries'  => '/usr/bin/ffmpeg',
        'ffprobe.binaries' => '/usr/bin/ffprobe',
));
```

### Composer can't create a cache directory

* When running Composer, you may notice the following warning:
```
Cannot create cache directory /home/$USER/.composer/cache/files/, or directory is not writable. Proceeding without cache
```
* You can specify Composer's cache directory with the environment variable `COMPOSER_CACHE_DIR=`. For Lychee, the cache is not necessary, and you can both disable it and hide the warning by specifying the location of the cache as `/dev/null` ([information](https://github.com/composer/composer/commit/fd6455218e304e9b484bebb0efcdb67bb52d051d)):
```bash
COMPOSER_CACHE_DIR='/dev/null' composer update --working-dir='/var/www/Lychee'
```

### I can't access the users under settings server error or api not found on Lightspeed

If you receive a server error or "api not found" error under lightspeed web server try going to `cPanel` > `Mod Security` and turning the feature off.

### I know my file permissions for `storage`, `bootstrap/cache`, `public/uploads`, and `public/dist` are correct and accessible by my web server user, but I'm still getting a PHP error when writing to any of these directories.

1. Make sure your PHP user and group is the same user and group as your web server by editing PHP's `www.conf`. For example, on a Fedora 32 Server system, the default user/group for php from Fedora's standard repo defaults to `apache`, even if you do not have Apache installed. 
2. On some operating systems with more restrictive SELinux rules (like Fedora 32 Server at the time of writing), you need to set the SELinux security context of these directories for them to be accessible by your web server user: 
```bash
chcon -R -t httpd_sys_rw_content_t storage
chcon -R -t httpd_sys_rw_content_t bootstrap/cache
chcon -R -t httpd_sys_rw_content_t public/uploads
chcon -R -t httpd_sys_rw_content_t public/dist
```

### I know port 80/443 are open on my machine, but Lychee/my server is still refusing all connections.

On some operating systems with more restrictive SELinux rules (like Fedora 32 Server at the time of writing), you need to allow your web server user to connect over the network with `setsebool -P httpd_can_network_connect on`. You can view the status of your SELinux booleans with `getsebool -a`.

### I know my SQL database is setup correctly, but Lychee is showing `SQLSTATE[HY000] [2002] No such file or directory` and is not able to make changes to the database. 

In `/var/www/html/Lychee/.env`, change `DB_HOST=localhost` to `DB_HOST=127.0.0.1`. Additionally, if `DB_PORT=` is not set, it should be set to `DB_PORT=3306` for mysql/MariaDB's default port, or whatever custom port you selected when configuring your SQL server software after installation. 

### Why are there messages about git files in my logs? (e.g. `.git/HEAD: failed to open stream: No such file or directory`)

Lychee checks for the presence of certain files to detect whether Lychee was installed as a git repository or from a release archive (.zip). It also uses them to determine which git commit is in use where applicable. This is included in the Diagnostics page to assist us in diagnosing issues.

These messages can be safely ignored.

### Import from server via symlink is failing.

If you check the logs and see an output like
```
2021-11-09 19:09:50 -- error -- App\Actions\Import\Exec::do -- 256 -- Could not import file (<path-to-image>/image.jpg): 0 symlink(): Operation not supported
```
this means that the native low-level function `symlink` fails, because the file system does not support symbolic links.
When using this import setting, symbolic links are created below the `/uploads` directory as this is the folder where the media files reside.

As a result, in order to use this function the file system on which `/uploads` resides must support symbolic links (e.g. ext3, ext4, btrfs, zfs, etc.).
_"Windows"_ file systems (e.g. NTFS, FAT, CIFS) won't work.
The directory with the original media files may still be a Windows file system.
This is, because symbolic links (as opposed to hard links) may cross file system boundaries.

### Uploads are not working with Cloudfare / behind a reverse proxy.

Check that the value of `TRUSTED_PROXIES` is correctly set in your `.env` file.

### I get an error SQLSTATE: Numeric value out of range: 1264 Out of range value for column 'legacy_id', what should I do?

Go into your _Settings_ then _More_, find `force_32bit_ids` in the _"config"_ section and set it to `1`.

---

## File: src/content/docs/docs/features/facial-recognition.md

---
title: Facial Recognition
sidebar:
  order: 10
  badge:
    text: v8
    variant: note
---

:::note
Facial recognition is part of Lychee v8 and is **disabled by default** while it matures. It also requires a separate microservice — it does not run inside the main Lychee application.
:::

Lychee can automatically detect faces in your photos, group similar-looking faces together, and let you label them as **People**. Once people are tagged you can browse "all photos of X" the same way you browse albums or tags, and even build [Person Albums](#people-and-person-albums) — smart albums that match one or more people.

All of the machine-learning work (face detection, embeddings, clustering, selfie matching) runs in a dedicated [Lychee-Facial-Recognition](https://github.com/LycheeOrg/Lychee-Facial-Recognition) microservice, kept separate from the main PHP application so the heavier workload doesn't run on your gallery server.

:::caution[Legal notice]
Facial recognition processes biometric data, which is subject to strict legal restrictions (or outright prohibited) in many jurisdictions — for example treated as "special category data" requiring an explicit legal basis under the EU GDPR. **You are solely responsible for verifying that operating this feature is lawful in your jurisdiction** and for obtaining any required consent before enabling it. By default Lychee shows a dismissable warning about this on the Face Clusters and Face Maintenance pages (controlled by the `ai_vision_face_recognition_warning` setting).
:::

## How it works

- **Auto-scan on upload** — newly uploaded photos are automatically queued for face detection once both AI Vision and facial recognition are enabled.
- **Detection** — the AI Vision service finds faces in each photo and returns bounding boxes plus an embedding (a numeric "fingerprint") for each face.
- **Clustering** — faces that aren't yet linked to a person are grouped into clusters of similar-looking faces, so you can label a whole group at once instead of one face at a time. Clustering can be re-run at any time from _Settings &rArr; Maintenance &rArr; Run Clustering_ (visible whenever unclustered faces exist) — useful after changing the microservice's `VISION_FACE_CLUSTER_EPS` if the resulting clusters aren't grouping people as expected. It only affects faces not yet assigned to a person; already-labelled faces are left untouched.
- **Overlays** — when viewing a photo, detected faces are shown as overlays. Press `P` to toggle their visibility.

## People and Person Albums

- **Person management** — create, rename, merge, and delete people from the People page.
- **Assign faces** — assign an individual detected face, or a whole face cluster, to a person.
- **Dismiss faces** — mark false-positive detections as dismissed so they stop appearing for review; admins (or photo owners, depending on the [permission mode](#permission-modes)) can also purge dismissed faces in bulk.
- **Person Albums** — smart albums that automatically include every photo containing one or more chosen people (matched with AND/OR logic), the same way [tag-based smart albums](/docs/usage/albums/) work.
- **Searchable people** — people can be excluded from search results individually (the `ai_vision_face_person_is_searchable_default` setting controls the default for newly created people).

## Claiming your own photos

Users can link a Person record to their own account ("claiming"), which is useful for letting people find themselves automatically:

- **Manual claim** — pick yourself from the People list.
- **Claim by selfie** — upload a selfie; Lychee forwards it to the AI Vision service's matching endpoint and automatically claims the best-matching person if the match confidence is above `ai_vision_face_selfie_confidence_threshold` (default `0.8`). The uploaded selfie itself is discarded immediately after matching — it is never stored.
- A person can only be claimed by one user at a time, and (unless you're an admin) you can't claim a second person once you already have one linked.
- Whether non-admin users may claim people at all is controlled by the `ai_vision_face_allow_user_claim` setting; admins can always claim/unclaim/merge regardless.

## Permission modes

Who can see and manage faces is controlled by a single setting, `ai_vision_face_permission_mode`:

| Operation              | `public`             | `private`            | `privacy-preserving`        | `restricted` (default)     |
|-------------------------|-----------------------|------------------------|--------------------------------|--------------------------------|
| View People page       | guest                | logged-in users      | photo/album owner + admin    | admin only                   |
| View face overlays     | album access         | logged-in users      | photo/album owner + admin    | photo/album owner + admin    |
| Create/edit a person    | logged-in users      | logged-in users      | photo/album owner + admin    | admin only                   |
| Assign a face           | logged-in users      | logged-in users      | photo/album owner + admin    | admin only                   |
| Trigger a scan          | logged-in users      | logged-in users      | photo/album owner + admin    | photo/album owner + admin    |
| Claim a person          | logged-in users      | logged-in users      | photo/album owner + admin    | photo/album owner + admin    |
| Merge people            | logged-in users      | logged-in users      | photo/album owner + admin    | admin only                   |
| Dismiss a face          | photo owner + admin  | photo owner + admin  | photo/album owner + admin    | photo owner + admin          |
| Batch face operations   | logged-in users      | logged-in users      | photo/album owner + admin    | admin only                   |
| View people on an album | album access         | logged-in users      | photo/album owner + admin    | photo/album owner + admin    |

## Settings

The feature is controlled by a `AI Vision` category of [Settings](/docs/getting-started/settings/), all disabled/conservative by default:

| Setting                                          | Description                                                              | Default        |
|----------------------------------------------------|-------------------------------------------------------------------------------|-----------------|
| `ai_vision_enabled`                              | Master toggle for the whole AI Vision subsystem (also gates NSFW classification). | off          |
| `ai_vision_face_enabled`                         | Toggle facial recognition specifically. Requires `ai_vision_enabled`.    | off            |
| `ai_vision_face_permission_mode`                 | See [Permission modes](#permission-modes).                              | `restricted`   |
| `ai_vision_face_selfie_confidence_threshold`     | Minimum match confidence (0.0–1.0) to auto-link via [selfie claim](#claiming-your-own-photos). | `0.8` |
| `ai_vision_face_person_is_searchable_default`    | Default searchability for newly created people.                         | on             |
| `ai_vision_face_allow_user_claim`                | Allow non-admin users to claim a person.                                 | on             |
| `ai_vision_face_overlay_enabled`                 | Master toggle for face overlays/bounding boxes in the UI.                | on             |
| `ai_vision_face_overlay_default_visibility`      | Whether overlays are shown or hidden by default when opening a photo (toggle with `P`). | visible |
| `ai_vision_face_recognition_warning`             | Show the dismissable legal warning on the Face Clusters/Maintenance pages. | on           |

## Self-hosting the AI Vision service

Facial recognition requires running the [Lychee-Facial-Recognition](https://github.com/LycheeOrg/Lychee-Facial-Recognition) service (FastAPI + DeepFace) alongside Lychee, then pointing Lychee at it via the `AI_VISION_FACE_URL` and `AI_VISION_FACE_API_KEY` environment variables — see [AI Vision](/docs/getting-started/configuration/#ai-vision-facial-recognition--nsfw-classification) in the configuration reference. The diagnostics page reports whether the service is reachable and correctly configured.

The service is configured independently, via its own `.env` file (copy `.env.example` to `.env`). All of its variables are prefixed `VISION_FACE_`; missing required variables produce a formatted error at startup instead of a raw traceback.

### Required

| Variable                       | Description                                                                  |
|----------------------------------|------------------------------------------------------------------------------|
| `VISION_FACE_LYCHEE_API_URL`   | Lychee base URL used for callbacks (no trailing slash).                     |
| `VISION_FACE_API_KEY`          | Shared API key — validated on inbound requests from Lychee and sent on outbound callbacks. Must match `AI_VISION_FACE_API_KEY` in Lychee's own `.env`. |

### Connection

| Variable                          | Default | Description                                                          |
|--------------------------------------|---------|---------------------------------------------------------------------------|
| `VISION_FACE_VERIFY_SSL`          | `true`  | Verify SSL certificates on Lychee callbacks. Set to `false` for self-signed certs. |
| `VISION_FACE_SKIP_LYCHEE_CHECK`   | `false` | Skip the Lychee connectivity check at startup (useful for local dev).     |

### Model

| Variable                         | Default            | Description                                                              |
|--------------------------------------|------------------------|-------------------------------------------------------------------------|
| `VISION_FACE_MODEL_NAME`         | `ArcFace`           | DeepFace recognition model.                                             |
| `VISION_FACE_DETECTOR_BACKEND`   | `retinaface`        | DeepFace detector backend (`retinaface`, `mtcnn`, `opencv`, `ssd`).      |
| `VISION_FACE_MODEL_ROOT`         | `/root/.deepface`   | Root directory for DeepFace model weights (`DEEPFACE_HOME`).            |

### Detection & matching

| Variable                            | Default | Description                                                            |
|------------------------------------------|---------|--------------------------------------------------------------------------|
| `VISION_FACE_DETECTION_THRESHOLD`   | `0.5`   | Bounding-box confidence filter.                                         |
| `VISION_FACE_MATCH_THRESHOLD`       | `0.5`   | Cosine-similarity cutoff for selfie matching and suggestions.           |
| `VISION_FACE_RESCAN_IOU_THRESHOLD`  | `0.5`   | IoU threshold for bounding-box matching on re-scan.                     |
| `VISION_FACE_MAX_FACES_PER_PHOTO`   | `10`    | Maximum faces included in a callback payload.                           |
| `VISION_FACE_MIN_FACE_SIZE_PIXELS`  | `0`     | Minimum face size in pixels; `0` disables this filter.                  |
| `VISION_FACE_BLUR_THRESHOLD`        | `0.5`   | Laplacian variance threshold; blurry faces below this are discarded.    |
| `VISION_FACE_CLUSTER_EPS`           | `0.3`   | DBSCAN epsilon (max cosine distance) used for face clustering.          |

### Storage

| Variable                      | Default              | Description                                                  |
|------------------------------------|---------------------------|---------------------------------------------------------------|
| `VISION_FACE_STORAGE_BACKEND` | `sqlite`              | `sqlite` or `pgvector`.                                      |
| `VISION_FACE_STORAGE_PATH`    | `/data/embeddings`    | SQLite database directory.                                   |
| `VISION_FACE_PG_HOST`         | `localhost`           | PostgreSQL host (`pgvector` backend only).                   |
| `VISION_FACE_PG_PORT`         | `5432`                | PostgreSQL port.                                              |
| `VISION_FACE_PG_DATABASE`     | `ai_vision`           | PostgreSQL database.                                          |
| `VISION_FACE_PG_USER`         | `ai_vision`           | PostgreSQL user.                                               |
| `VISION_FACE_PG_PASSWORD`     | _empty_               | PostgreSQL password.                                          |
| `VISION_FACE_PHOTOS_PATH`     | `/data/photos`        | Shared volume mount where the service reads photo files from. Mount Lychee's `LYCHEE_UPLOADS` directory here, read-only. |

### Job queue

Detection and clustering jobs are processed asynchronously via a persistent queue shared across all worker processes. Requests that arrive when the queue is full receive `429 Too Many Requests`.

| Variable                      | Default     | Description                                                  |
|------------------------------------|------------------|---------------------------------------------------------------|
| `VISION_FACE_QUEUE_BACKEND`   | `database`  | `database` (uses `VISION_FACE_STORAGE_BACKEND`) or `redis`.  |
| `VISION_FACE_QUEUE_MAX_SIZE`  | `100`       | Maximum number of pending jobs.                               |
| `VISION_FACE_REDIS_HOST`      | `localhost` | Redis host (`redis` backend only).                            |
| `VISION_FACE_REDIS_PORT`      | `6379`      | Redis port.                                                   |
| `VISION_FACE_REDIS_PASSWORD`  | _empty_     | Redis password.                                                |
| `VISION_FACE_REDIS_DB`        | `0`         | Redis logical database index.                                  |

### Concurrency

| Variable                          | Default | Description                                                          |
|--------------------------------------|---------|---------------------------------------------------------------------------|
| `VISION_FACE_THREAD_POOL_SIZE`    | `1`     | Inference threads; also sets the number of queue worker tasks.            |
| `VISION_FACE_WORKERS`             | `1`     | Uvicorn worker processes.                                                  |

### Docker example

```bash
docker run --rm \
  --env-file .env \
  -v /path/to/lychee/public/uploads:/data/photos:ro \
  -v ai-vision-embeddings:/data/embeddings \
  -p 8000:8000 \
  lychee-ai-vision
```

The container exposes interactive API docs at `/docs` and a health check at `/health` once running.

## Command-line tools

| Command                                  | Description                                                                 |
|---------------------------------------------|----------------------------------------------------------------------------------|
| `php artisan lychee:scan-faces`          | Enqueue all unscanned photos for face detection. Use `--album={id}` to limit to the direct photos of one album. |
| `php artisan lychee:rescan-failed-faces` | Re-enqueue photos whose scan previously failed. Add `--stuck-pending` (with `--older-than=<minutes>`, default 60) to also reset scans stuck in "pending" back to unscanned. |

## Troubleshooting

**Facial recognition options don't appear in Settings:**
- Check that `ai_vision_enabled` and `ai_vision_face_enabled` are both turned on. Facial recognition is disabled by default and requires both.

**Diagnostics reports the AI Vision service as unreachable:**
- Verify `AI_VISION_FACE_URL` and `AI_VISION_FACE_API_KEY` are set in Lychee's `.env` and match the `VISION_FACE_API_KEY` configured on the microservice side.
- Confirm the microservice container is running and reachable from Lychee (`docker-compose logs` on the facial-recognition service, or check its `/health` endpoint).
- If Lychee and the microservice are on different hosts/networks, make sure firewalls and Docker networks allow the connection.

**Photos are never scanned / stay stuck on "pending":**
- Confirm a Lychee queue worker is running — face scans are processed asynchronously like other jobs.
- Run `php artisan lychee:rescan-failed-faces --stuck-pending` to reset scans that have been stuck in "pending" for longer than `--older-than` minutes (default 60).
- Check the microservice logs for `429 Too Many Requests`, which means its job queue (`VISION_FACE_QUEUE_MAX_SIZE`) is full; either wait for it to drain or increase the limit.

**Some faces in a photo are detected but others are missed:**
- Check `VISION_FACE_DETECTION_THRESHOLD` (default `0.5`) on the microservice — faces below this confidence are dropped; lower it to catch more faces at the cost of more false positives.
- Small, angled, or partially occluded faces are the most likely to be missed. `VISION_FACE_MIN_FACE_SIZE_PIXELS` (default `0`, i.e. no filter) can be raising the bar further if set above zero — make sure it isn't filtering out legitimately small faces.
- Blurry faces are discarded via `VISION_FACE_BLUR_THRESHOLD` (default `0.5`, Laplacian variance) — lower it if sharp-enough faces are still being rejected.
- `VISION_FACE_MAX_FACES_PER_PHOTO` (default `10`) caps how many faces are returned per photo; group photos with more people than this will only have the top matches included.
- Try a different `VISION_FACE_DETECTOR_BACKEND` (`retinaface`, `mtcnn`, `opencv`, `ssd`) — detectors vary in recall depending on pose, lighting, and image resolution.

**Clustering groups unrelated faces together, or fails to group similar faces:**
- Clustering is controlled by `VISION_FACE_CLUSTER_EPS` (default `0.3`), the DBSCAN epsilon (maximum cosine distance) allowed within a cluster. Lower it if dissimilar people are being grouped together; raise it if the same person is being split across multiple clusters.
- To apply a new value: update `VISION_FACE_CLUSTER_EPS` in the microservice's `.env`, restart the microservice so it picks up the change, then re-trigger clustering from _Settings &rArr; Maintenance &rArr; Run Clustering_ in Lychee. This re-clusters every currently unassigned face with the new epsilon — repeat with a different value if the result still isn't right.
- The `VISION_FACE_MODEL_NAME` (default `ArcFace`) recognition model determines embedding quality — a different model may produce more consistent embeddings for your photo set, but changing it does not retroactively update existing embeddings.
- Poor detections (blurry, low-confidence, or partial faces — see above) produce noisy embeddings that cluster poorly; fixing detection quality often improves clustering as a side effect.
- Clusters are only formed from faces not yet linked to a person; once a face is assigned to a person it's removed from the pool of unclustered faces.

**Faces are detected but nothing shows up when opening a photo:**
- Press `P` to toggle overlay visibility — they may just be hidden.
- Check `ai_vision_face_overlay_enabled` and `ai_vision_face_overlay_default_visibility`.
- Overlays and the People page are gated by `ai_vision_face_permission_mode` — see [Permission modes](#permission-modes); as an ordinary user you may simply lack the rights to view them.

**Selfie claim doesn't match the right person:**
- The match confidence must exceed `ai_vision_face_selfie_confidence_threshold` (default `0.8`). Lower it if legitimate matches are being rejected, or raise it if false positives occur.
- Make sure the person you expect to match has enough labelled faces for a reliable embedding average.

**The microservice fails to start:**
- Missing required environment variables (`VISION_FACE_LYCHEE_API_URL`, `VISION_FACE_API_KEY`) produce a formatted error at startup — check the container logs for which variable is missing.
- If using self-signed certificates for the Lychee callback URL, set `VISION_FACE_VERIFY_SSL=false`.
- For local development without a reachable Lychee instance, set `VISION_FACE_SKIP_LYCHEE_CHECK=true`.

---

## File: src/content/docs/docs/features/flow.md

---
title: Flow
sidebar:
  order: 6
---

Flow displays albums in a feed-like manner, similar to social media platforms. Each album becomes a scrollable card — with a cover image, a carousel of the photos inside, and metadata such as its date range — giving you a visually engaging alternative to browsing the album tree.

## How it works

- Flow starts from a base album (the gallery root by default, or a specific album set via `flow_base`) and turns eligible **descendants** of that base into cards — the base album itself is never shown as a card.
- By default (`flow_include_sub_albums` off), only the **direct children** of the base are considered — nested sub-albums further down the tree are not included. Enabling `flow_include_sub_albums` includes every descendant at any depth; if no base album is set either, this effectively means every album in the gallery is a candidate.
- Only albums that directly contain photos become cards. Enabling `flow_include_photos_from_children` also includes albums with no photos of their own by showing photos from their descendants instead — **not recommended**, since it can be slow and memory-intensive on large trees.
- Album descriptions are rendered as Markdown on the card.
- The owner's name is only shown to logged-in viewers; anonymous visitors (when `flow_public` is enabled) never see it.
- Cards are loaded a page at a time as you scroll, rather than all at once.
- Flow is exposed as its own standalone page and respects the same album-level permissions as the rest of the gallery — a user only sees cards for albums they're allowed to access.

### Ordering and the `opt-in` strategy

`flow_strategy` controls both which albums are eligible and how the feed is sorted:

- `auto` (default) — every eligible album is included, newest-created first.
- `opt-in` — only albums that have been explicitly published to the feed are included, sorted by their publish date (newest first).

:::note
`opt-in` relies on each album having a publish timestamp, but at the time of writing there is no interface action exposed to set this per album. Until one ships, switching `flow_strategy` to `opt-in` will result in an empty Flow feed — stick with `auto` (the default) for a usable feed today.
:::

### Sensitive (NSFW) albums in Flow

Albums marked as sensitive are handled the same way as elsewhere in Lychee, via the [Sensitive Albums settings](/docs/getting-started/settings/#sensitive-albums):

- `hide_nsfw_in_flow` (default: on) — excludes sensitive albums from Flow entirely.
- `flow_blur_nsfw_enabled` (default: on) — if sensitive albums aren't hidden, blur their photos on the card until a user clicks through.

## Settings

The feature is controlled by a `Flow` category of [Settings](/docs/getting-started/settings/):

| Setting                                 | Description                                                                                       | Default          |
|------------------------------------------|-----------------------------------------------------------------------------------------------------|-------------------|
| `flow_enabled`                          | Master toggle for the Flow view.                                                                   | on                |
| `flow_public`                           | Allow anonymous (non-logged-in) users to access Flow.                                              | off               |
| `flow_base`                             | Album ID used as the root of the feed. Leave empty to use the gallery root.                        | empty             |
| `flow_max_items`                        | Number of albums loaded per Flow page. Lower means more requests; higher means more memory usage.  | `10`              |
| `flow_strategy` SE              | See [Ordering and the opt-in strategy](#ordering-and-the-opt-in-strategy).                          | `auto` |
| `flow_include_sub_albums` SE    | Include all descendants of the base album, not just its direct children.                           | off               |
| `flow_include_photos_from_children` SE | Show photos from child albums when an album itself has none. **Not recommended** — can cause memory exhaustion and slowdowns on large trees. | off |
| `flow_open_album_on_click` SE  | Navigate to the album when a card is clicked, instead of opening the photo viewer directly.        | off               |
| `flow_display_open_album_button` SE | Show an explicit "Open Album" button on each card.                                             | off               |
| `flow_image_header_enabled` SE | Show an image header at the top of each card, featuring the album cover.                            | on                |
| `flow_image_header_cover` SE   | How the header image fills the card: `cover` crops to fill, `fit` scales to fit.                    | `cover`           |
| `flow_image_header_height` SE  | Height of the image header, in `rem`.                                                               | `24`              |
| `flow_carousel_enabled` SE     | Show a photo carousel below the image header on each card.                                          | on                |
| `flow_carousel_height` SE      | Height of the photo carousel, in `rem`.                                                              | `6`               |
| `flow_highlight_first_picture` SE | Use the album's first photo as the main image instead of its cover photo.                        | on                |
| `flow_min_max_enabled` SE      | Show the min/max date range of the album's photos on each card.                                     | on                |
| `flow_min_max_order`                    | Which date (older or newer) is shown first in the min/max date range.                              | `older_younger`   |
| `flow_display_statistics` SE   | Show view, share, and download counts on each card. Also requires the [`metrics_enabled`](/docs/getting-started/settings/#metrics_enabled) master toggle and read-metrics permission on the album. | on |
| `flow_compact_mode_enabled` SE | Clamp the description to 3 lines and hide extra details (photo/child counts); a "Show more" button expands the card. | on |

`flow_base`, `flow_max_items`, and `flow_min_max_order` are available on the free edition; the rest of the card's layout and the inclusion strategy require the [Supporter Edition](/docs/se/overview/).

### Date format

Two settings accept [PHP date format strings](https://www.php.net/manual/en/datetime.format.php) and require the Supporter Edition:

| Key                            | Default                | Used for                              |
|----------------------------------|------------------------------|----------------------------------------|
| `date_format_flow_published`   | `M j, Y, g:i:s A e`     | Album published/created date shown on cards |
| `date_format_flow_min_max`      | `F Y`                    | Min/max date range shown on cards     |

## Supporter Edition Features

With the [Supporter Edition](/docs/se/overview/) you get finer control over the feed:

- **Opt-in strategy** — switch `flow_strategy` from `auto` (every eligible album) to `opt-in` (only explicitly published albums); see the [caveat above](#ordering-and-the-opt-in-strategy) about its current front-end support.
- **Nested album inclusion** — pull in every descendant of the base album, not just its direct children, via `flow_include_sub_albums`.
- **Compact mode** — clamp cards to a shorter, summarized layout via `flow_compact_mode_enabled`.
- **Extensive card customization** — toggle the image header, carousel, statistics, and min/max date range independently, and tune their sizing.

---

## File: src/content/docs/docs/features/frame.md

---
title: Frame
sidebar:
  order: 8
---

Frame turns Lychee into a digital photo frame: opening `/frame` goes fullscreen and cycles through random photos one at a time, each shown centered over a blurred, full-bleed copy of itself.

## How it works

- Each cycle picks a random photo — from a specific album if `random_album_id` is configured, or otherwise from every photo you're allowed to search across the whole gallery.
- Videos are skipped: if a random pick turns out to be a video, Frame retries (up to 5 times) before giving up for that cycle.
- Sensitive (NSFW) photos are excluded automatically when `hide_nsfw_in_frame` — a [Sensitive Albums setting](/docs/getting-started/settings/#sensitive-albums) — is enabled.
- Press `Esc`, or use the on-screen back button, to exit fullscreen and return to wherever you started Frame from.

## Accessing Frame

- Frame is reached at `/frame`, or via its entry in the left menu.
- The menu entry is only shown when `mod_frame_enabled` is on **and** the current user can access the album configured by `random_album_id`. If that album is private and the viewer isn't logged in (or lacks the rights), the menu entry disappears for them — even though the route itself would still work for someone who does have access.
- By default, `random_album_id` points at the built-in [Highlighted smart album](/docs/features/smart-albums/) (your starred photos). Set it to any album ID to restrict Frame to that album, or clear it to draw from every searchable photo in the gallery.

## Settings

| Setting              | Description                                                                 | Default       |
|------------------------|---------------------------------------------------------------------------------|-----------------|
| `mod_frame_enabled`  | Master toggle for Frame mode.                                               | on            |
| `random_album_id`    | Album ID used as the photo source. Leave empty to draw from all searchable photos. | `highlighted` |
| `mod_frame_refresh`  | Seconds between photo changes.                                              | `30`          |

`hide_nsfw_in_frame`, under [Sensitive Albums](/docs/getting-started/settings/#sensitive-albums), additionally controls whether sensitive photos can appear in the rotation.

## Building a custom frame client

Besides the built-in fullscreen view, Lychee also exposes a `Photo::random` endpoint (see the [API reference](/docs/administration/api/)) that returns a full photo resource — not just an image URL — using the same random-selection logic (album source, video skipping, NSFW exclusion) as the built-in Frame. This is meant for driving your own external photo-frame client, for example on a Raspberry Pi or another dedicated display, when you need more than a bare image URL.

## Use Cases

- Digital photo frame displays on dedicated screens
- Waiting room or reception area photo slideshows
- Gallery exhibition displays

---

## File: src/content/docs/docs/features/map.md

---
title: Map
sidebar:
  order: 4
---

The Map module displays your geotagged photos on an interactive map, showing where your photos were taken.

## Features

- Interactive map with photo markers
- Cluster markers for areas with many photos
- Click markers to preview photos
- Filter by album or date range

## Requirements

Photos must contain GPS data in their EXIF metadata for map placement. Most modern smartphones and GPS-enabled cameras embed this data automatically.

## Configuration

The map module can be enabled or disabled in the [Settings](/docs/getting-started/settings/). You can also configure the map provider and default zoom level.

---

## File: src/content/docs/docs/features/rss.md

---
title: RSS Feed
sidebar:
  order: 9
---

Lychee can publish a feed of your most recently added photos, so RSS readers can notify subscribers as your library grows.

## How it works

- The feed lists photos added within the last `rss_recent_days` days (default: `7`), newest first, capped at `rss_max_items` entries (default: `100`).
- It reflects whatever's searchable to whoever requests it — the same visibility rules as [Search](/docs/features/search/). Since RSS readers don't carry your Lychee login session, in practice this means the feed only ever shows publicly accessible photos.
- Each entry links to the photo in the gallery, and its enclosure is the **original, full-resolution** file — not a thumbnail — so keep bandwidth in mind if `rss_max_items` is set high.
- Descriptions are rendered as Markdown; the author is the photo owner's display name (or username if none is set); the category is the containing album's title.
- Sensitive (NSFW) photos are excluded when `hide_nsfw_in_rss` — a [Sensitive Albums setting](/docs/getting-started/settings/#sensitive-albums) — is enabled.

## Accessing the feed

Despite the setting being named `rss_enable`, the feed is actually served at **`/feed`**, not `/rss` — subscribe to `https://your-lychee-instance/feed`. Requesting it while `rss_enable` is off returns an error rather than an empty feed.

## Settings

| Setting            | Description                                          | Default |
|----------------------|-----------------------------------------------------------|-----------|
| `rss_enable`       | Master toggle for the feed at `/feed`.               | off     |
| `rss_max_items`    | Maximum number of items in the feed.                 | `100`   |
| `rss_recent_days`  | Only include photos uploaded within the last X days. | `7`     |

`hide_nsfw_in_rss`, under [Sensitive Albums](/docs/getting-started/settings/#sensitive-albums), additionally excludes sensitive photos from the feed.

---

## File: src/content/docs/docs/features/search.md

---
title: Search
sidebar:
  order: 3
---

Lychee provides search functionality to quickly find photos and albums across your library.

## Search Capabilities

- Search by photo title and description
- Search by album name
- Search by tags
- Search across your entire accessible library

## Public Search

Administrators can enable or disable public search to allow unauthenticated visitors to search through public albums.

---

## File: src/content/docs/docs/features/sensitive-albums.md

---
title: Sensitive Albums
sidebar:
  order: 2
---

Any album can be flagged **sensitive**. This is a built-in feature — toggled directly in the album's settings. A sensitive album gets blurred in listings, can be gated behind a click-to-consent warning, and is excluded by default from smart albums, search, RSS, the timeline, the map, and Frame.

## Marking an album as sensitive

- **Manually** — toggle it in the album's protection policy, the same place you set an album to public/private, require a link, or set a password.
- **Automatically** — [NSFW Classification](/docs/se/nsfw-classification/) can mark an album as sensitive on its own when a "sensitive"-tier finding is detected in one of its photos.
- **Recursively** — sub-albums of a sensitive album are treated as sensitive too, without needing the flag set on each of them individually. Marking a sub-album of an already-sensitive album is skipped, since the parent already covers it.

## What viewers see

| Behavior                  | Setting                    | Default | Effect when on                                                                 |
|-------------------------------|---------------------------------|---------|--------------------------------------------------------------------------------------|
| List sensitive sub-albums    | `nsfw_visible`               | on      | Sensitive sub-albums still appear in listings (blurred, see below). When off, they're omitted from listings entirely. |
| Blur the cover thumbnail    | `nsfw_blur`                   | off     | The album's thumbnail/cover is blurred wherever it's listed.                       |
| Warn guests on open          | `nsfw_warning`                | off     | Anonymous visitors get a click-to-consent overlay the first time they open the album. |
| Warn logged-in users on open | `nsfw_warning_admin`          | off     | Same, but for authenticated users — set independently from the guest setting.       |
| Custom warning text          | `nsfw_banner_override`        | _empty_ | Raw HTML shown in the warning overlay instead of the built-in translated text. **Not sanitized** — only put trusted content here. |
| Blur instead of solid backdrop | `nsfw_banner_blur_backdrop` | off     | The warning overlay blurs the album behind it instead of showing a solid dark-red panel. |

The warning overlay (when shown) requires a single click anywhere on it to dismiss; the album's contents are blurred/hidden behind it until then. Consent is only remembered for the current browser session — it isn't persisted, so the warning reappears on the next visit.

Note that `nsfw_warning`/`nsfw_warning_admin` only gate the *click-through prompt*; turning both off does not unmark the album as sensitive — it's still blurred (if `nsfw_blur` is on) and still excluded from smart albums/search/etc. below, just without the interstitial.

## Excluded from smart albums, search, and more

By default, sensitive content is left out of most aggregate views across the gallery — each area has its own independent toggle, all on by default:

| Area          | Setting                        |
|-------------------|-------------------------------------|
| Smart albums    | `hide_nsfw_in_smart_albums`       |
| Search          | `hide_nsfw_in_search`              |
| RSS feed        | `hide_nsfw_in_rss`                 |
| Timeline        | `hide_nsfw_in_timeline`            |
| Map             | `hide_nsfw_in_map`                 |
| Frame           | `hide_nsfw_in_frame`               |

See [Smart Albums](/docs/features/smart-albums/#per-user-behavior) for more on how this interacts with smart-album visibility specifically.

---

## File: src/content/docs/docs/features/slideshow.md

---
title: Slideshow
sidebar:
  order: 7
---

Lychee includes a built-in slideshow mode that auto-advances through a set of photos, with a brief fade-to-black transition between each one.

## How it works

- Photos advance automatically every `slideshow_timeout` seconds (default: `5`).
- Reaching the last photo loops back to the first (and vice versa) if `photos_wraparound` is enabled — otherwise the slideshow simply stops there.
- Videos are not skipped: the slideshow waits for a video to finish playing before advancing, rather than switching after the usual timeout.

  :::caution
  This wait relies on the video actually starting to play. If `autoplay_enabled` is turned off, a video reached during a slideshow will never autoplay and therefore never fire its "ended" event — the slideshow will appear to hang on that video until you manually skip past it.
  :::
- You can still navigate manually with the left/right arrow keys (see [Keyboard Shortcuts](/docs/usage/keyboard/)) while a slideshow is running; doing so shows that photo immediately and restarts the auto-advance timer from there.

## Starting a slideshow

A "Play" button starts the slideshow wherever you're viewing a set of photos: from an album's header, from within the photo viewer's toolbar, or from Tag results, Search results, the Timeline, or a Person's photos. The toolbar and other UI chrome automatically fade out once the slideshow is running (reappearing on hover on larger screens) to keep the view uncluttered; opening the toolbar and pressing the button again stops the slideshow.

## Settings

| Setting               | Description                                                                 | Default |
|--------------------------|-----------------------------------------------------------------------------------|-----------|
| `slideshow_enabled`   | Master toggle for the slideshow "Play" button.                              | on      |
| `slideshow_timeout`   | Seconds between photos while a slideshow is running.                       | `5`     |
| `photos_wraparound`   | Loop back to the first photo after the last one (and vice versa).          | on      |
| `autoplay_enabled`    | Set the `autoplay` attribute on video elements — see the caution above; disabling it can stall slideshows on video photos. | on      |

---

## File: src/content/docs/docs/features/smart-albums.md

---
title: Smart Albums
sidebar:
  order: 1
---

Smart Albums collect photos automatically based on a condition, instead of requiring you to assign photos to them by hand. Lychee has two kinds:

- **Built-in smart albums** — system-wide, virtual albums (Unsorted, Recent, rating tiers, etc.) that you can only turn on or off, not create or delete.
- **User-created smart albums** — real albums you create yourself, that pick their photos dynamically by matching **tags** or **people** instead of by manual assignment. They behave like regular albums otherwise (your own title, cover photo, sharing/access settings).

Both kinds re-evaluate their contents live, so a photo appears or disappears the moment it stops/starts matching — there's nothing to keep in sync manually.

## Built-in smart albums

| Album              | Shows                                                                                          | Setting (default)            |
|------------------------|------------------------------------------------------------------------------------------------------|----------------------------------|
| Unsorted            | Photos not assigned to any album.                                                                   | `enable_unsorted` (on)         |
| Highlighted          | Photos you've highlighted (the old "starred" flag).                                                | `enable_highlighted` (on)      |
| Recent               | Photos uploaded within the last `recent_age` days.                                                  | `enable_recent` (on), `recent_age` (`1` day) |
| On This Day          | Photos taken on today's month/day in a previous year (falls back to upload date if there's no EXIF date). | `enable_on_this_day` (on) |
| Untagged             | Photos with no tags at all.                                                                          | `enable_untagged` (on)         |

:::caution
Disabling `enable_unsorted` doesn't delete anything, but it does make photos that aren't in any album effectively invisible in the gallery — there's no other built-in way to reach them.
:::

:::tip
`recent_age` defaults to just **1 day**. If "Recent" feels emptier than you'd expect, that's almost certainly why — raise it in Settings.
:::

Highlighting a photo is really just a shortcut for giving it a 5-star rating from yourself — see below.

## Rating-based smart albums

Photos can be rated 1–5 stars, by any user with access, and Lychee averages everyone's rating into `rating_avg` per photo. The "stars" smart albums filter on that average:

| Album        | Range                          | Setting (default)              |
|------------------|------------------------------------|------------------------------------|
| Unrated       | No ratings at all                | `enable_unrated` (off)           |
| 1 Star        | `1.0 ≤ rating_avg < 2.0`          | `enable_1_star` (off)            |
| 2 Stars       | `2.0 ≤ rating_avg < 3.0`          | `enable_2_stars` (off)           |
| 3 Stars       | `rating_avg ≥ 3.0` (3★ and up)    | `enable_3_stars` (off)           |
| 4 Stars       | `rating_avg ≥ 4.0` (4★ and up)    | `enable_4_stars` (**on**)        |
| 5 Stars       | `rating_avg ≥ 5.0`                | `enable_5_stars` (**on**)        |

Note that 1★ and 2★ are exact bins, while 3★ and 4★ are cumulative thresholds (a 5-star photo shows up in all of "3 Stars", "4 Stars", and "5 Stars").

Two more rating-based albums look at ratings rather than photos directly:

| Album                | Shows                                                          | Setting (default)                                    |
|---------------------------|---------------------------------------------------------------------|------------------------------------------------------------|
| My Rated Pictures      | Every photo *you've* personally rated, sorted by your own rating. | `enable_my_rated_pictures` (on) — logged-in users only |
| Best Pictures           | The top `best_pictures_count` photos by `rating_avg` (ties at the cutoff are all included). | `enable_best_pictures` (on), `best_pictures_count` (`100`) — requires [Supporter Edition](/docs/se/overview/) |
| My Best Pictures        | The top `my_best_pictures_count` photos *you've* rated, by your own rating. | `enable_my_best_pictures` (on), `my_best_pictures_count` (`50`) — requires [Supporter Edition](/docs/se/overview/), logged-in users only |

## Per-user behavior

Each logged-in user effectively gets their own view of every smart album — not because there are separate albums per user, but because Lychee's normal visibility rules (what photos you're allowed to see) are applied before the smart-album condition. Two settings adjust this further:

- `enable_smart_album_per_owner` — restricts smart albums to *only* photos you own, instead of every photo you have access to.
- `SA_override_visibility` (expert setting) — the opposite extreme: makes any photo matching the smart-album condition visible through that album, bypassing normal photo visibility entirely. Use with care.

Smart albums that are about organizing your own contributions (Unsorted, Highlighted, Recent, On This Day, Unrated, Untagged) are only shown to users with upload rights; the rating-tier and "Best/My" albums are shown to everyone regardless.

By default, photos in albums marked sensitive by [NSFW Classification](/docs/se/nsfw-classification/) are excluded from every smart album — controlled by `hide_nsfw_in_smart_albums`, on by default. This is one of several independent per-feature toggles (search, RSS, timeline, map, and Frame each have their own equivalent setting).

## User-created smart albums

Unlike the built-in ones above, these are albums you explicitly create, with your own title and (optionally) a chosen cover photo — they just don't have photos manually assigned to them. Instead, you pick a set of tags or people and a match mode:

- **Tag Albums** — automatically include every photo carrying one or more chosen tags, matched with AND/OR logic. See [Tagging](/docs/features/tagging/).
- **Person Albums** — automatically include every photo containing one or more chosen people, matched with AND/OR logic. See [Facial Recognition](/docs/features/facial-recognition/#people-and-person-albums).

Like the built-in albums, both re-evaluate live — tag or re-tag a photo (or re-run face detection) and matching tag/person albums update immediately. An expert setting, `TA_override_visibility`, mirrors `SA_override_visibility` above but for tag albums specifically.

---

## File: src/content/docs/docs/features/tagging.md

---
title: Tagging
sidebar:
  order: 3
---

Tags provide a flexible way to categorize and find photos across your entire library.

:::note
Tags are shared, global entities — if two users both tag a photo `car`, they're using the same underlying tag. Management actions (rename, merge, delete) only ever touch **your own** photos; other users' photos keep the tag untouched, unless you're an admin, in which case they apply gallery-wide. A tag is only fully removed once no photo anywhere references it anymore.
:::

## Tag Management

Any user with upload rights (not just admins) can manage tags from the Tag Management view:

- **Rename tags** — internally this finds (or creates) a tag with the new name and migrates your photos to it, then removes the old name from your photos. If another user still has photos under the old name, their tag is left alone.
- **Merge tags** — combine one tag into another; your photos (and any of your [Tag Albums](#tag-albums)) referencing the source tag are moved to the destination tag.
- **Delete tags** — remove a tag from your own photos. The tag itself disappears once no one's photos reference it anymore.
- **Tag management view** — lists every tag with a photo count. As a non-admin, the count and the tag's photo listing only ever reflect **your own** photos, even though the tag name may be shared with other users.

## Using Tags

- **Tag descriptions** — besides a name, a tag can also have a description.
- **Photo listing per tag** — view all of your own photos under a specific tag (admins see everyone's).
- **Auto-completion** — the tag input suggests existing tags (with their photo counts) as you type. When tagging a photo, you can also add a brand-new tag by typing a name that doesn't exist yet; when picking tags to build a Tag Album, only existing tags are offered.
- **Bulk tagging** — apply tags to multiple selected photos at once, either adding them alongside existing tags or overriding (replacing) whatever tags those photos already had.

## Tag Albums

Tag Albums are dynamic, tag-driven smart albums: pick one or more tags and a match mode, and every photo carrying **any** of them (OR) or **all** of them (AND) is included automatically — no manual sorting required. Like other smart albums, they re-evaluate live: tag or re-tag a photo and matching Tag Albums update immediately. See [Smart Albums](/docs/features/smart-albums/#user-created-smart-albums) for how they compare to the built-in smart albums, and the `TA_override_visibility` setting below for making their contents visible independently of each photo's own visibility.

## Settings

| Setting                        | Description                                                                                   | Default |
|-----------------------------------|-----------------------------------------------------------------------------------------------------|-----------|
| `TA_override_visibility`       | Tag Album visibility overrides individual photo visibility, making matching photos publicly accessible regardless of their own settings. | off |
| `hide_nsfw_in_tag_albums`      | Hide sensitive photos from Tag Albums.                                                        | on      |
| `hide_nsfw_in_tag_listing`     | Hide sensitive photos from the per-tag photo listing.                                          | on      |
| `photo_thumb_tags_enabled` SE | Show tags on photo thumbnails in the album view. Has no effect when `photo_thumb_info` is set to `description`. | off |

The NSFW-related settings live under [Sensitive Albums](/docs/getting-started/settings/#sensitive-albums); `TA_override_visibility` and `photo_thumb_tags_enabled` live under [Smart & Featured Albums](/docs/getting-started/settings/#smart--featured-albums) and [Gallery](/docs/getting-started/settings/#gallery) respectively.

## Supporter Edition Features

With the [Supporter Edition](/docs/se/overview/):

- **Display tags on album view** — show tags directly on photo thumbnails in the album grid, via `photo_thumb_tags_enabled`.

---

## File: src/content/docs/docs/features/timeline.md

---
title: Timeline
sidebar:
  order: 5
---

The Timeline provides a chronological view of your photos, making it easy to browse your library by date.

## Features

- **Standalone page** — access the timeline as a dedicated page
- **Direct date access** — jump to a specific date
- **Access control** — respects album-level permissions

## Supporter Edition Features

With the [Supporter Edition](/docs/se/overview/):

- **Custom granularity** — choose the time grouping (day, week, month, year)
- **Custom format** — customize the date display format
- **Customizable timeline per album** — configure timeline settings on a per-album basis

---

## File: src/content/docs/docs/getting-started/configuration.md

---
title: "Configuration"
sidebar:
  order: 3
---

Lychee's core configuration is managed using a `.env` file. It probably exists already in your directory, but if not it can be created by copying `.env.example`. The options which are already included in the `.env` file should be sufficient to cover the necessary configuration for the vast majority of all use-cases and setups. Using other options than those included in the `.env` file should rarely be necessary. However, this page contains a more complete list of the available options, incl. some highly advanced ones, together with descriptions and default values.
For non-core options (for example UI options), take a look at [Settings](/docs/getting-started/settings/).

:::caution[Docker users]
If you are using the Docker image, changes to environment variables require a container restart, as the server loads and caches the configuration at startup.
:::

### Base options

| Option               | Description                                                                            | Default                     |
|----------------------|------------------------------------------------------------------------------------------|-----------------------------|
| `APP_NAME`           | The gallery name                                                                       | `Lychee`                    |
| `APP_ENV`            | Environment of your gallery. Only the literal value `production` is treated as production; anything else (e.g. `local`, `development`) is non-production, see [below](#environment-values). | `production`                |
| `APP_DEBUG`          | Show detailed error messages with stack traces instead of a generic error page. Also exposes all environment variables on uncaught exceptions, see [Hiding Environment Variables From Debug Pages](#hiding-environment-variables-from-debug-pages). Never enable on a public-facing install. | `false` |
| `APP_URL`            | The hostname of your gallery (which should resolves to the `public/` folder).          | `http://localhost`          |
| `APP_DIR`            | The subfolder path part of the URL, for installs hosted under a sub-path instead of a domain root. We do not recommend its use, see [below](#app_url-and-app_dir). | _empty_                     |
| `APP_KEY`            | Your app key which is used for encryption (set during installation)                    | `null`                      |
| `ASSET_URL`          | Overrides the base URL used to load built JS/CSS assets. Rarely needed.                | `null`                      |
| `TIMEZONE`           | The timezone of your photos, requires a named timezone identifier like `Europe/Paris` | system timezone of server   |
| `LYCHEE_UPLOADS`     | Path to uploads directory                                                              | `uploads/` inside `public/` |
| `LYCHEE_UPLOADS_DIR` | Subfolder name used to build `LYCHEE_UPLOADS` and its URL when `LYCHEE_UPLOADS` is unset | `uploads/`                 |
| `LYCHEE_UPLOADS_URL` | URL to uploads directory, better left empty                                            | `/uploads`                  |
| `APP_FORCE_HTTPS`    | Force HTTPS on all URLs                                                                | `false`                     |
| `TRUSTED_PROXIES`    | Trusted proxy IP addresses                                                             | `null`                      |
| `LOG_VIEWER_ENABLED` | Enable log viewer inside Lychee instead of through CLI.                                | `true`                      |
| `WHITE_LABEL_ENABLED` | Hide all Lychee branding from the UI. Requires a valid [SE licence](/docs/se/white-label/).  | `false`                     |
| `KEYGEN_API_KEY`     | API token from [keygen.lycheeorg.dev](https://keygen.lycheeorg.dev). When set, an expired [SE licence](/docs/se/overview/) is automatically rotated on admin login, and the diagnostics page warns if the token itself is about to expire. | `null` |

### `APP_URL` and `APP_DIR`

:::note
`APP_URL` must only contain the hostname up to the Top Level Domain (tld) _e.g._ .com, .org etc.
If you are using Lychee in a sub folder, specify the path after the tld here in the `APP_DIR` constant.
For example for `https://lychee.test/path/to/lychee`:
- set `APP_URL` to `https://lychee.test`
- and set `APP_DIR` to `/path/to/lychee`
:::

:::caution[We do not recommend the use of `APP_DIR`]
`APP_DIR` exists to support hosting Lychee from a sub-path instead of a domain (or sub-domain) root. Every part of Lychee that generates a URL — image/thumbnail URLs, the custom CSS asset, WebAuthn, the log viewer — has to consistently account for that prefix. Leaving `APP_DIR` unset while actually deploying under a sub-path (or getting it slightly wrong) is a common cause of broken images/assets and broken WebAuthn login; the diagnostics page has dedicated checks for exactly this failure mode.

Whenever possible, prefer giving Lychee its own (sub)domain root (e.g. `photos.example.com`) over a path-based sub folder (e.g. `example.com/photos`). It sidesteps this whole class of issues.
:::

:::tip
For `TIMEZONE`, do not use an offset (`+01:00`) or an abbreviation (`CEST`)
:::

### Environment values

`APP_ENV` only has special meaning to Lychee and its dependencies when it is exactly `production`:

- **`production`** (default) — the safe default for any public-facing install. Generic error pages are shown instead of stack traces, `APP_DEBUG_LATENCY` is always ignored, and the Content Security Policy is enforced.
- **Anything else** (e.g. `local`, `development`) — treated as non-production. This is what unlocks the [Development options](#development-options) below, and is required by the Log Viewer if you want to use it outside of an admin session (per the official `.env.example`: "it is not possible to enable Log Viewer in production; if you wish to enable it, also switch your `APP_ENV` to `local`").

Never set `APP_ENV` to anything other than `production` on a public-facing install — combined with `APP_DEBUG`, non-production values relax several security defaults.

### Feature flags

These options enable or disable entire features of Lychee. Most of them are advanced and rarely need to be touched.

| Option                                  | Description                                                                                                     | Default |
|------------------------------------------|-------------------------------------------------------------------------------------------------------------------|---------|
| `LOG_404_ERRORS`                        | Log all 404 errors, useful to track broken links or attempted attacks. Set to `false` to avoid overly large logs. | `true`  |
| `LEGACY_V4_REDIRECT`                    | Redirect legacy v4 links of the form `/#albumID/photoID` to the new URL scheme.                                  | `false` |
| `S3_ENABLED`                            | Use an S3-compatible bucket instead of local storage for media. Also requires `AWS_ACCESS_KEY_ID` to be set, see [AWS](#aws). | `false` |
| `REQUIRE_CONTENT_TYPE_ENABLED`          | Require all API requests to set a `content-type` header. Disabling this allows using the API from the API documentation page. | `true`  |
| `DISABLE_BASIC_AUTH`                    | Disable username/password login. Only API tokens, WebAuthn or OAuth can then be used to authenticate. Only enable this after setting up another login method. | `false` |
| `DISABLE_WEBAUTHN`                      | Disable WebAuthn login.                                                                                          | `false` |
| `DISABLE_IMPORT_FROM_SERVER`            | Disable the "import from server" feature, reducing the attack surface if the admin account is ever compromised. | `false` |
| `WEBSHOP_ENABLED`                       | Enable the [webshop](/docs/webshop/) feature.                                                                    | `true`  |
| `WEBHOOK_ENABLED`                       | Enable outgoing webhooks, triggered on photo lifecycle events (`photo.add`, `photo.move`, `photo.delete`).       | `false` |
| `WEBHOOK_TIMEOUT_SECONDS`               | Seconds Lychee waits for a webhook endpoint to respond before treating the request as failed.                    | `10`    |
| `LOCATION_DECODING_REQUESTS_PER_SECOND` | Rate limit for reverse-geocoding requests sent to the Nominatim service. Only raise this if you run your own instance. | `1` |
| `VULNERABILITY_CHECK_ENABLED`           | Periodically compare the running version against published security advisories and surface matches on the diagnostics page and login. | `true` |
| `UPDATE_CHECK_ENABLED`                  | Allow admins to check from the dashboard whether the installation is up to date.                                 | `true`  |
| `USE_FOPEN_FOR_URL_IMPORTS`             | Use PHP's `fopen` instead of curl for URL imports. curl mitigates _Time of Check Time of Use_ (TOCTOU) issues but isn't available everywhere.  | `false` |
| `USE_SYSTEM_TEMP_DIR`                   | Use PHP's system temp directory for chunked uploads. Set to `false` on shared hosting where it isn't readable/writable, to use `storage/tmp/uploads_parts` instead. | `true` |
| `ENABLE_REQUEST_CACHING`                | Expose Redis-backed response caching settings (`cache_enabled`, `cache_ttl`, `cache_event_logging`) in the admin settings panel. | `false` |
| `HIDE_LYCHEE_SE_CONFIG`                 | Hide Lychee SE options from the configuration screens, useful for recording demos.                               | `false` |
| `V8_ENABLED`                            | Enable experimental, in-development v8 features. Not recommended outside of testing the next major version.      | `false` |

### Database options

Lychee supports MySQL/MariaDB, SQLite and PostgreSQL as database backends. Microsoft SQL Server can be used, but is unsupported. The configuration is managed using the `DB_` variables.

#### `DATABASE_URL`

Some hosting providers give you a single URL containing all the information needed to configure your database. Therefore, Lychee has a `DATABASE_URL` option which only needs the database connection type. For example:

```ini
DB_CONNECTION=mysql
DATABASE_URL="mysql://root:password@127.0.0.1/forge?charset=UTF-8"
```

If this applies to your hosting provider, you can skip the other DB configuration steps.

#### MySQL/MariaDB

The configuration is exactly the same for both systems.

| Option              | Value it should have                                                                                    |
|---------------------|-----------------------------------------------------------------------------------------------------------|
| `DB_CONNECTION`     | `mysql`                                                                                                 |
| `DB_HOST`           | Host of the database server (if it's running on the same server use `127.0.0.1`)                        |
| `DB_PORT`           | Port of the database server (default 3306)                                                              |
| `DB_DATABASE`       | The name of the database                                                                                |
| `DB_USERNAME`       | Username of the database user                                                                           |
| `DB_PASSWORD`       | Password of the database user                                                                           |
| `DB_SOCKET`         | UNIX socket to DB server, replaces `DB_HOST` and `DB_PORT`                                              |
| `MYSQL_ATTR_SSL_CA` | Optional and only used when using the `pdo_mysql` extension, file path to the SSL certificate authority |
| `DB_POOL_MIN`       | Minimum number of pooled connections (Octane only)                                                       | 1 |
| `DB_POOL_MAX`       | Maximum number of pooled connections (Octane only)                                                       | 10 |

#### SQLite

| Option          | Value it should have                                           |
|-----------------|------------------------------------------------------------------|
| `DB_CONNECTION` | `sqlite`                                                       |
| `DB_DATABASE`   | Path to the database file (default `database/database.sqlite`) |

#### PostgreSQL

| Option          | Value it should have                                                                            |
|-----------------|---------------------------------------------------------------------------------------------------|
| `DB_CONNECTION` | `pgsql`                                                                                         |
| `DB_HOST`       | Host of the database server (if it's running on the same server use `127.0.0.1`) or socket path |
| `DB_PORT`       | Port of the database server or `null` if using socket (default 5432)                            |
| `DB_DATABASE`   | The name of the database                                                                        |
| `DB_USERNAME`   | Username of the database user                                                                   |
| `DB_PASSWORD`   | Password of the database user                                                                   |



#### Migrating from Lychee v3

:::caution
To migrate from Lychee v3 you *must* use the same MySQL/MariaDB server as v3.
:::

| Option                 | Description                                                                | Default  |
|------------------------|-------------------------------------------------------------------------------|----------|
| `DB_OLD_LYCHEE_PREFIX` | Table prefix (e.g. `lychee_`) of the Lychee v3 instance to migrate from.    | _empty_  |

### Mailer options

Mail is required if you want to use the [`new_photos_notification`](/docs/getting-started/settings/#new_photos_notification) setting, which emails users a weekly digest when new photos are added to albums shared with them.

Supported mailers are `smtp`, `ses`, `mailgun`, `postmark` or `sendmail`, which you can set using `MAIL_DRIVER`. There is also a `log` driver (see [Testing your configuration](#testing-your-configuration) below).

#### General options

| Option              | Description    |
|---------------------|----------------|
| `MAIL_DRIVER`       | Mailer type: `smtp`, `ses`, `mailgun`, `postmark`, `sendmail` or `log` |
| `MAIL_FROM_ADDRESS` | "From" address |
| `MAIL_FROM_NAME`    | "From" name (defaults to `APP_NAME`) |
| `MAIL_EHLO_DOMAIN`  | Local domain announced in the SMTP `EHLO`/`HELO` command. Advanced, rarely needed. |

#### SMTP

| Option            | Description                                |
|-------------------|----------------------------------------------|
| `MAIL_HOST`       | Host of SMTP server                        |
| `MAIL_PORT`       | Port of SMTP server (default 587)          |
| `MAIL_ENCRYPTION` | Encryption for SMTP server: `tls` (STARTTLS, default), `ssl` (implicit TLS, typically used with port 465), or left empty for no encryption |
| `MAIL_USERNAME`   | Username of SMTP server                    |
| `MAIL_PASSWORD`   | Password of SMTP server                    |

Most providers (Gmail, Outlook/Office 365, your ISP...) publish the exact `MAIL_HOST`, `MAIL_PORT` and `MAIL_ENCRYPTION` values to use for SMTP on their support pages — search for "\ SMTP settings".

#### SES

SES can be configured using AWS settings. See [AWS configuration](#aws).

#### Mailgun

| Option             | Description                                  |
|--------------------|-------------------------------------------------|
| `MAILGUN_DOMAIN`   | Domain of the Mailgun server                 |
| `MAILGUN_SECRET`   | Secret of the Mailgun server                 |
| `MAILGUN_ENDPOINT` | Mailgun endpoint (default `api.mailgun.net`) |

#### Postmark

| Option           | Description        |
|------------------|-----------------------|
| `POSTMARK_TOKEN` | Token for Postmark |

#### sendmail

| Option              | Description                                  | Default                          |
|---------------------|-------------------------------------------------|-----------------------------------|
| `MAIL_SENDMAIL_PATH` | Path (and arguments) to the local `sendmail` binary | `/usr/sbin/sendmail -bs -i` |

#### Testing your configuration

Set `MAIL_DRIVER=log` to write outgoing emails to `storage/logs/laravel.log` instead of actually sending them. This lets you confirm that Lychee is attempting to send mail (and inspect its contents) without needing a fully working SMTP setup or risking spamming real users while you experiment with the other `MAIL_` options.

Once configured, you can trigger the notification job manually to test end-to-end instead of waiting for its weekly schedule:

```bash
php artisan lychee:photos_added_notification
```

:::note
Notification emails are only sent by [Laravel's task scheduler](https://laravel.com/docs/scheduling#running-the-scheduler), which itself must be invoked once a minute by a system `cron` job (or systemd timer) pointing at `php artisan schedule:run`. See the [FAQ](/docs/faq/general/) for an example. Without this, `new_photos_notification` will never trigger on its own even if mail sending itself works.
:::

### Cache options

Lychee can use various services as cache driver to store temporary data. The driver is set using `CACHE_DRIVER` and supports: `apc`, `array`, `file`, `memcached`, `redis` or `dynamodb`.

#### General options

| Option         | Description                                   |
|----------------|--------------------------------------------------|
| `CACHE_PREFIX` | Prefix of cache data keys in in-memory stores |
| `CACHE_STORE`  | Alias for `CACHE_DRIVER`. If both are set, `CACHE_STORE` takes precedence. |

#### Memcached

| Option                    | Description                 |
|---------------------------|---------------------------------|
| `MEMCACHED_HOST`          | Host for memcached          |
| `MEMCACHED_PORT`          | Port for memcached          |
| `MEMCACHED_USERNAME`      | Username for memcached      |
| `MEMCACHED_PASSWORD`      | Password for memcached      |
| `MEMCACHED_PERSISTENT_ID` | Persistent ID for memcached |

#### DynamoDB

Base options are configured using [AWS options](#aws). You need to create a table, please refer to the [Laravel docs](https://laravel.com/docs/8.x/cache#dynamodb).

| Option                 | Description       | Default |
|------------------------|----------------------|---------|
| `DYNAMODB_CACHE_TABLE` | Cache table name  | `cache` |
| `DYNAMODB_ENDPOINT`    | DynamoDB endpoint | `null`  |

#### Redis

Also see [Redis](#redis).

| Option           | Description          |
|------------------|--------------------------|
| `REDIS_CACHE_DB` | Redis cache database |

#### Database

There are no config options, however, you need to run `php artisan cache:table` to use this option.

#### Log Viewer cache

| Option                    | Description                                                                                   | Default |
|---------------------------|---------------------------------------------------------------------------------------------------|---------|
| `LOG_VIEWER_CACHE_DRIVER` | Cache driver used by the Log Viewer. If you use Redis as your main cache driver, it is strongly recommended to set this to `file` instead — should Redis crash, you would otherwise lose access to your logs. | `file`  |
| `LOG_STDOUT`              | Also send logs to stdout, useful when running in a container and collecting logs from the container runtime. | `false` |
| `LOG_VIEWER_API_ONLY`     | Only expose the Log Viewer through its API, without the bundled UI.                          | `false` |

### Services

Lychee can interact with various third-party services. You can find config options for them here.

#### Redis

To use Redis, you need the [PhpRedis](https://github.com/phpredis/phpredis) PHP extension.

| Option           | Description                                                  |
|------------------|--------------------------------------------------------------------|
| `REDIS_SCHEME`   | Redis connection scheme (default `tcp`, other option `unix`) |
| `REDIS_PATH`     | Redis Unix socket path                                       |
| `REDIS_HOST`     | Redis host                                                   |
| `REDIS_PASSWORD` | Redis password                                               |
| `REDIS_PORT`     | Redis port                                                   |
| `REDIS_DB`       | Default Redis database index (used outside of caching, e.g. sessions/queue) | `0` |
| `REDIS_CLUSTER`  | Redis cluster                                                |
| `REDIS_PREFIX`   | Redis prefix                                                 |
| `REDIS_URL`      | Redis URL                                                    |

#### AWS

| Option                  | Description                                               |
|--------------------------|----------------------------------------------------------------|
| `AWS_ACCESS_KEY_ID`     | Access key ID for AWS                                     |
| `AWS_SECRET_ACCESS_KEY` | Secret access key for AWS                                 |
| `AWS_DEFAULT_REGION`    | Default AWS region                                        |
| `AWS_URL`               | [S3] Overrides viewing URL, for use with CDNs and similar |
| `AWS_BUCKET`            | [S3] The bucket to use                                    |
| `AWS_ENDPOINT`          | [S3] The endpoint for uploads. `AWS_URL` defaults to this |
| `AWS_IMAGE_VISIBILITY`  | [S3] Visibility of uploaded objects (`public` or `private`) | `public` |
| `AWS_USE_PATH_STYLE_ENDPOINT` | [S3] Use path-style endpoint URLs, required by some S3-compatible providers (e.g. MinIO) | `false` |

:::tip
Setting `AWS_ACCESS_KEY_ID` only configures the AWS credentials; it does not by itself make Lychee store media on S3. Also set `S3_ENABLED=true`, see [Feature flags](#feature-flags).
:::

#### LDAP

Lychee can authenticate users against an LDAP or Active Directory server, alongside or instead of local accounts.

| Option                    | Description                                                                                       | Default                                  |
|----------------------------|--------------------------------------------------------------------------------------------------------|--------------------------------------------|
| `LDAP_ENABLED`            | Enable LDAP authentication.                                                                       | `false`                                  |
| `LDAP_HOST`               | LDAP server hostname.                                                                              | `ldap.example.com`                       |
| `LDAP_PORT`               | LDAP server port. Use `636` for LDAPS.                                                             | `389`                                     |
| `LDAP_BASE_DN`            | Base DN for LDAP searches.                                                                         | `dc=example,dc=com`                      |
| `LDAP_BIND_DN`            | DN of the service account used to bind and search LDAP. This account only needs read-only access. | `cn=bind-user,dc=example,dc=com`         |
| `LDAP_BIND_PASSWORD`      | Password of the bind account.                                                                      | _empty_                                  |
| `LDAP_CONNECTION_TIMEOUT` | Connection timeout, in seconds.                                                                    | `5`                                       |
| `LDAP_USE_TLS`            | Use TLS for the LDAP connection.                                                                   | `true`                                    |
| `LDAP_TLS_VERIFY_PEER`    | Verify the LDAP server's TLS certificate.                                                          | `true`                                    |
| `LDAP_USER_FILTER`        | LDAP search filter used to find a user by username (`%s` is replaced with the submitted username). OpenLDAP: `(&(objectClass=person)(uid=%s))`. Active Directory: `(&(objectClass=user)(sAMAccountName=%s))`. | `(&(objectClass=person)(uid=%s))` |
| `LDAP_ATTR_USERNAME`      | LDAP attribute mapped to the Lychee username.                                                     | `uid`                                     |
| `LDAP_ATTR_EMAIL`         | LDAP attribute mapped to the Lychee email address.                                                | `mail`                                    |
| `LDAP_ATTR_DISPLAY_NAME`  | LDAP attribute mapped to the Lychee display name.                                                 | `displayName`                             |
| `LDAP_ADMIN_GROUP_DN`     | DN of an LDAP group whose members are granted admin rights in Lychee.                             | `null`                                    |
| `LDAP_AUTO_PROVISION`     | Automatically create Lychee accounts on first successful LDAP login. If `false`, users must already exist in Lychee. | `true` |
| `LDAP_LOGGING`            | Log LDAP bind/search operations, useful for debugging. Passwords are never logged.                | `false`                                   |

#### OAuth providers

Lychee can delegate login to a number of external OAuth/OpenID providers. Each provider needs its own client ID and secret, obtained from the provider. Unless stated otherwise, `*_REDIRECT_URI` should be left at its default value.

| Provider     | Required options                                                                 | Notes                                                                                  |
|--------------|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| Amazon       | `AMAZON_SIGNIN_CLIENT_ID`, `AMAZON_SIGNIN_SECRET`                                | `AMAZON_SIGNIN_REDIRECT_URI` defaults to `/auth/amazon/redirect`.                       |
| Apple        | `APPLE_CLIENT_ID`, `APPLE_CLIENT_SECRET`                                         | The client secret is a JWT with a maximum lifetime of 6 months and must be regenerated periodically. `APPLE_REDIRECT_URI` defaults to `/auth/apple/redirect`. |
| Authelia     | `AUTHELIA_CLIENT_ID`, `AUTHELIA_CLIENT_SECRET`, `AUTHELIA_BASE_URL`              | For self-hosted Authelia instances. Also set `AUTHELIA_REDIRECT_URI`.                   |
| Authentik    | `AUTHENTIK_CLIENT_ID`, `AUTHENTIK_CLIENT_SECRET`, `AUTHENTIK_BASE_URL`           | For self-hosted Authentik instances. Also set `AUTHENTIK_REDIRECT_URI`.                 |
| Facebook     | `FACEBOOK_CLIENT_ID`, `FACEBOOK_CLIENT_SECRET`                                   | `FACEBOOK_REDIRECT_URI` defaults to `/auth/facebook/redirect`.                          |
| GitHub       | `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`                                       | `GITHUB_REDIRECT_URI` defaults to `/auth/github/redirect`.                              |
| Google       | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`                                       | `GOOGLE_REDIRECT_URI` defaults to `/auth/google/redirect`.                              |
| Keycloak     | `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_CLIENT_SECRET`, `KEYCLOAK_BASE_URL`, `KEYCLOAK_REALM` | For self-hosted Keycloak instances. Also set `KEYCLOAK_REDIRECT_URI`.                   |
| Mastodon     | `MASTODON_DOMAIN`, `MASTODON_ID`, `MASTODON_SECRET`                              | `MASTODON_DOMAIN` is the URL of your Mastodon instance, e.g. `https://mastodon.social`. `MASTODON_REDIRECT_URI` defaults to `/auth/mastodon/redirect`. |
| Microsoft    | `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`                                 | `MICROSOFT_TENANT_ID` defaults to `common`. `MICROSOFT_REDIRECT_URI` defaults to `/auth/microsoft/redirect`. |
| Nextcloud    | `NEXTCLOUD_CLIENT_ID`, `NEXTCLOUD_CLIENT_SECRET`, `NEXTCLOUD_BASE_URI`           | For self-hosted Nextcloud instances. Also set `NEXTCLOUD_REDIRECT_URI`.                 |

### AI Vision (facial recognition & NSFW classification)

These options configure the connection to the external AI Vision microservices used for [facial recognition](/docs/features/facial-recognition/) and [NSFW classification](/docs/se/nsfw-classification/). They are kept out of the database-backed settings so the service URL and shared API key are never exposed through the admin settings UI.

| Option                                       | Description                                                                       | Default |
|------------------------------------------------|----------------------------------------------------------------------------------------|---------|
| `AI_VISION_ENABLED`                           | Enable AI Vision features (facial recognition, person management, photo clustering). | `true`  |
| `AI_VISION_FACE_URL`                          | URL of the external facial-recognition service.                                  | _empty_ |
| `AI_VISION_FACE_API_KEY`                      | API key for the facial-recognition service.                                      | _empty_ |
| `AI_VISION_FACE_RESCAN_IOU_THRESHOLD`         | Intersection-over-union threshold used to decide whether a detected face needs rescanning. | `0.3` |
| `AI_VISION_FACE_STUCK_SCAN_THRESHOLD_MINUTES` | Minutes after which a face-scan job that hasn't progressed is considered stuck.   | `720`   |
| `AI_VISION_NSFW_URL`                          | URL of the external NSFW-classification service.                                 | _empty_ |
| `AI_VISION_NSFW_API_KEY`                      | API key for the NSFW-classification service.                                     | _empty_ |

### Payments

Payment options gate the [webshop](/docs/webshop/) checkout (requires Pro). See [Payments](/docs/webshop/payments/) for the full setup guide for each provider.

| Option                  | Description                                                                  | Default |
|--------------------------|----------------------------------------------------------------------------------|---------|
| `OMNIPAY_TEST_MODE`     | Use sandbox/test mode for payment gateways. Only set to `false` once you're ready to accept real payments. | `true` |
| `MOLLIE_API_KEY`        | Mollie API key.                                                              | _empty_ |
| `MOLLIE_PROFILE_ID`     | Mollie profile ID.                                                           | _empty_ |
| `PAYPAL_CLIENT_ID`      | PayPal client ID.                                                            | _empty_ |
| `PAYPAL_SECRET`         | PayPal secret.                                                               | _empty_ |

### Authentication Guards

Support for token based authentication used by API requests.

| Name                                  | Description                                                                                          | Default |
|-----------------------------------------|------------------------------------------------------------------------------------------------------------|---------|
| `ENABLE_BEARER_TOKEN_AUTH`            | Authentication Guard token support. Falls back to `ENABLE_TOKEN_AUTH` if unset.                     | `true`  |
| `ENABLE_TOKEN_AUTH`                   | Legacy name for `ENABLE_BEARER_TOKEN_AUTH`, kept for backwards compatibility.                       | `true`  |
| `REMEMBER_LIFETIME`                   | Duration, in minutes, of the "Remember me" login cookie.                                            | `40320` (4 weeks) |
| `FAIL_NO_AUTHENTICABLE_BEARER_TOKEN`  | Throw an exception when a bearer token is supplied but no matching user is found. Only applies when `ENABLE_BEARER_TOKEN_AUTH` is `true`. | `true` |
| `LOG_WARN_NO_BEARER_TOKEN`            | Log a warning when an API token is supplied without the `Bearer` scheme prefix.                     | `true`  |


### Session and security options

Sessions are stored in the same locations as [cache](#cache). You change the used driver using `SESSION_DRIVER`.

| Name                          | Description                                                                                        | Default |
|---------------------------------|------------------------------------------------------------------------------------------------------|---------|
| `SESSION_LIFETIME`            | Idle session expiration in minutes; the session will need to be reinitialized once it has expired. | 120     |
| `SESSION_SECURE_COOKIE`       | Cookies only via HTTPS                                                                             | `false` |
| `SECURITY_HEADER_HSTS_ENABLE` | Enable HTTP strict transport security                                                              | `false` |

#### Content Security Policy (CSP)

These options add extra allowed sources to Lychee's default Content Security Policy. Each accepts a comma-separated list of origins.

| Option                                  | CSP directive extended         |
|--------------------------------------------|--------------------------------------|
| `SECURITY_HEADER_CSP_CHILD_SRC`           | `child-src`                        |
| `SECURITY_HEADER_CSP_CONNECT_SRC`         | `connect-src`                      |
| `SECURITY_HEADER_CSP_FONT_SRC`            | `font-src`                         |
| `SECURITY_HEADER_CSP_FORM_ACTION`         | `form-action`                      |
| `SECURITY_HEADER_CSP_FRAME_ANCESTORS`     | `frame-ancestors` (also drives the `X-Frame-Options` header and relaxes the session cookie's `SameSite` attribute from `lax` to `none` when set) |
| `SECURITY_HEADER_CSP_FRAME_SRC`           | `frame-src`                        |
| `SECURITY_HEADER_CSP_IMG_SRC`             | `img-src`                          |
| `SECURITY_HEADER_CSP_MEDIA_SRC`           | `media-src`                        |
| `SECURITY_HEADER_SCRIPT_SRC_ALLOW`        | `script-src` (additional allowed script hosts) |

#### Advanced options

:::note
These config options are advanced config options. Do not change them unless you know what you are doing.
:::


| Option           | Description                                                                             |
|--------------------|---------------------------------------------------------------------------------------------|
| `APP_CIPHER`     | The app's cipher suite                                                                  |
| `HASHING_ALGORITHM` | Hashing algorithm for passwords (default `bcrypt`, other options `argon` or `argon2id`) |
| `ARGON_MEMORY`   | Memory for Argon hashing algorithm                                                      |
| `ARGON_THREADS`  | Threads for Argon hashing algorithm                                                     |
| `ARGON_TIME`     | Time for Argon hashing algorithm                                                        |
| `BCRYPT_ROUNDS`  | Rounds for bcrypt hashing algorithm                                                     |
| `WEBAUTHN_NAME`  | Name shown to users for WebAuthn devices (defaults to `APP_NAME`)                       |
| `WEBAUTHN_ID`    | Relying party ID for WebAuthn devices                                                   |
| `SESSION_COOKIE` | The cookie's name (defaults to a slug of `APP_NAME` followed by `_session`)             |
| `SESSION_STORE`  | Cache store used for cache-backed session drivers (`apc`, `dynamodb`, `memcached`, `redis`) |
| `SESSION_DOMAIN` | Session cookie domain                                                                   |
| `SKIP_DIAGNOSTICS_CHECKS` | Comma-separated list of class names of diagnostics checks that should be skipped. | _empty_ |
| `DB_LIST_FOREIGN_KEYS` | List foreign keys on the diagnostics page. Only takes effect when `APP_DEBUG` is also `true`. | `false` |

### Development options

:::note
Don't use this in productive environments. May affect stability and performance.
:::


Of these, only `APP_DEBUG_LATENCY` is hard-gated by [`APP_ENV`](#environment-values) in code (it is a no-op while `APP_ENV` is `production`). The others aren't technically blocked in production, but doing so is unsupported and may affect stability, performance, or expose debugging information.

| Option               | Description                                            | Default |
|------------------------|-------------------------------------------------------------|---------|
| `APP_DEBUG_LATENCY`  | Add this many milliseconds of artificial latency before processing requests. Always disabled when `APP_ENV` is `production`, regardless of this value. | `0` |
| `DEBUGBAR_ENABLED`   | Enable debugbar. This also disables the Content Security Policy. | `false` |
| `DB_LOG_SQL`         | Log SQL statements, see your Logs within Lychee.       | `false` |
| `DB_LOG_SQL_EXPLAIN` | Explain the SQL statements for MySQL.                  | `false` |
| `DB_LOG_SQL_MIN_TIME` | Only log SQL statements whose execution time exceeds this many milliseconds. | `100` |
| `XHPROF_ENABLED`     | Enable the XHProf profiler.                            | `false` |
| `CLOCKWORK_ENABLE`   | Enable the [Clockwork](https://underground.works/clockwork/) debugging toolbar. | `false` |
| `CLOCKWORK_DRIVER`   | Clockwork data collection driver.                      | `laravel` |
| `CLOCKWORK_STORAGE_FILES_PATH` | Path used to store Clockwork's collected request data. | `storage/clockwork` |
| `VITE_LOCAL_DEV`     | Enable local Vite development without running a separate dev server. | `false` |
| `VITE_HTTP_PROXY_TARGET` | Target URL for the Vite HTTP proxy used for API calls during local frontend development. | _empty_ |

### Advanced configuration

:::note
Only for advanced users familiar with PHP and Laravel. Never do this unless you know what you are doing.
:::


You can look at the files in the `config/` folder. They contain some options you can't configure using environmental variables and you are able to adapt Lychee completely to your needs.

## Environment Configuration

It is often helpful to have different configuration values based on the environment where the application is running. For example, you may wish to use a different cache driver locally than you do on your production server.

To make this a cinch, Lychee utilizes the [DotEnv][1] PHP library by Vance Lucas. In a fresh Lychee installation, the root directory of your application will contain a `.env.example` file. You should make a copy of this file as `.env`.

Your `.env` file should not be committed to your application's source control, since each developer / server using your application could require a different environment configuration. Furthermore, this would be a security risk in the event an intruder gains access to your source control repository, since any sensitive credentials would get exposed.

If you are developing with a team, you may wish to continue including a `.env.example` file with your application. By putting placeholder values in the example configuration file, other developers on your team can clearly see which environment variables are needed to run your application. You may also create a `.env.testing` file. This file will override the `.env` file when running PHPUnit tests or executing Artisan commands with the `--env=testing` option.

:::tip
Any variable in your `.env` file can be overridden by external environment variables such as server-level or system-level environment variables.
:::


### Environment Variable Types

All variables in your `.env` files are parsed as strings, so some reserved values have been created to allow you to return a wider range of types from the `env()` function:

| `.env` Value | `env()` Value |
|--------------|---------------|
| true         | (bool) true   |
| (true)       | (bool) true   |
| false        | (bool) false  |
| (false)      | (bool) false  |
| empty        | (string) ''   |
| (empty)      | (string) ''   |
| null         | (null) null   |
| (null)       | (null) null   |

If you need to define an environment variable with a value that contains spaces, you may do so by enclosing the value in double quotes.

```ini
APP_NAME="My Application"
```

### Retrieving Environment Configuration
All of the variables listed in this file will be loaded into the $_ENV PHP super-global when your application receives a request. However, you may use the env helper to retrieve values from these variables in your configuration files. In fact, if you review the Lychee configuration files, you will notice several of the options already using this helper:

```php
'debug' => env('APP_DEBUG', false),
```
The second value passed to the env function is the "default value". This value will be used if no environment variable exists for the given key.


### Determining The Current Environment
The current application environment is determined via the APP_ENV variable from your .env file. You may access this value via the environment method on the App facade:

```php
$environment = App::environment();
```
You may also pass arguments to the environment method to check if the environment matches a given value. The method will return true if the environment matches any of the given values:

```php
if (App::environment('development')) {
    // The environment is development
}

if (App::environment(['development', 'production'])) {
    // The environment is either development OR production...
}
```


:::tip
The current application environment detection can be overridden by a server-level `APP_ENV` environment variable. This can be useful when you need to share the same application for different environment configurations, so you can set up a given host to match a given environment in your server's configurations.
:::


### Hiding Environment Variables From Debug Pages

When an exception is uncaught and the `APP_DEBUG` environment variable is `true`, the debug page will show all environment variables and their contents. In some cases you may want to obscure certain variables. You may do this by updating the `debug_blacklist` option in your `config/app.php` configuration file.

Some variables are available in both the environment variables and the server / request data. Therefore, you may need to blacklist them for both `$_ENV` and `$_SERVER`:

```php
return [

    // ...

    'debug_blacklist' => [
        '_ENV' => [
            'APP_KEY',
            'DB_PASSWORD',
        ],

        '_SERVER' => [
            'APP_KEY',
            'DB_PASSWORD',
        ],

        '_POST' => [
            'password',
        ],
    ],
];
```

## Accessing Configuration Values

You may easily access your configuration values using the global `config` helper function from anywhere in your application. The configuration values may be accessed using "dot" syntax, which includes the name of the file and option you wish to access. A default value may also be specified and will be returned if the configuration option does not exist:

```php
$value = config('app.timezone');
```
To set configuration values at runtime, pass an array to the config helper:

```php
config(['app.timezone' => 'America/Chicago']);
```

## Configuration Caching

To give your application a speed boost, you should cache all of your configuration files into a single file using the `config:cache` Artisan command. This will combine all of the configuration options for your application into a single file which will be loaded quickly by the framework.

You should typically run the `php artisan config:cache` command as part of your production deployment routine. The command should not be run during local development as configuration options will frequently need to be changed during the course of your application's development.

:::note
If you execute the `config:cache` command during your deployment process, you should be sure that you are only calling the `env` function from within your configuration files. Once the configuration has been cached, the `.env` file will not be loaded and all calls to the `env` function will return `null`.
:::



## Maintenance Mode

When your application is in maintenance mode, a custom view will be displayed for all requests into your application. This makes it easy to "disable" your application while it is updating or when you are performing maintenance. A maintenance mode check is included in the default middleware stack for your application. If the application is in maintenance mode, a `MaintenanceModeException` will be thrown with a status code of 503.

To enable maintenance mode, execute the `down` Artisan command:

```bash
php artisan down
```
You may also provide `message` and `retry` options to the `down` command. The `message` value may be used to display or log a custom message, while the `retry` value will be set as the `Retry-After` HTTP header's value:

```bash
php artisan down --message="Upgrading Database" --retry=60
```
Even while in maintenance mode, specific IP addresses or networks may be allowed to access the application using the command's `allow` option:

```bash
php artisan down --allow=127.0.0.1 --allow=192.168.0.0/16
```

To disable maintenance mode, use the `up` command:

```bash
php artisan up
```
:::tip
You may customize the default maintenance mode template by defining your own template at `resources/views/errors/503.blade.php`.
:::


[1]: https://github.com/vlucas/phpdotenv

---

## File: src/content/docs/docs/getting-started/contributions.md

---
title: "Contributing"
sidebar:
  order: 6
---

To encourage active collaboration, Lychee strongly encourages pull requests, not just bug reports. "Bug reports" may also be sent in the form of a pull request containing a failing test.

However, if you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible to demonstrate the issue. We provide a template and expect it to be respected. Most bug reports that do not follow it will likely be closed. The goal of a bug report is to make it easy for yourself &mdash;and others&mdash; to replicate the bug and develop a fix.

Remember, bug reports are created in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the bug report will automatically see any activity or that others will jump in to fix it. Creating a bug report serves to help yourself and others start on the path to fixing the problem. If you want to chip in, you can help out by fixing any bugs listed in our [issue trackers][1].

## Support Questions

Lychee's GitHub issue trackers are not intended to provide Lychee help or support. Please instead create a discussion [here][3] or contact us directly on [discord][2].

## Security Vulnerabilities

Lychee uses a rolling release system, **we do not backport fixes to previously released versions**.
Those are the versions in which we accept vulnerability reports.

| Version          | Supported |
|------------------|-----------|
| master           | &#10004;  |
| latest release   | &#10004;  |
| < latest release | &#10005;  |
| < 7.0            | &#10005;  |

### Reporting a Vulnerability

If you discover a security vulnerability within Lychee, please contact us directly on [Discord][2] or open a draft advisory [here][5]. All security vulnerabilities will be promptly addressed.

:::tip
In order to help us triage your report, we encourage you to provide us with a Proof of Concept (PoC) demonstrating the vulnerability. This will help us understand the issue and prioritize it accordingly. You can use our [demo environment](https://demo.lycheeorg.dev) to test your PoC (non-destructively). For destructive testing, please use your own environment and provide us with a clear explanation of the steps to reproduce the issue.
:::

Please **do not report** security **vulnerabilities which are direct output of your own tools**. Make sure to verify the issue manually and provide a clear explanation of the vulnerability, including steps to reproduce it. We are starting to see a lot of reports which are generated by automated AI scanners and contain hallucinations or do not consider parts of the codebase.

:::caution
For each low quality report that is closed, you gain 1 strike: ⚾ **3 strikes (⚾⚾⚾) and you will be blocked from this repository.**
:::

### Well known false positives

We have seen a few reports mentioning issues which we consider part of the intended design of Lychee.

#### About the api/v2/Diagnostics endpoint

If you are thinking about reporting an issue regarding the `api/v2/Diagnostics` endpoint, please note that **it is intentionally public and does not require authentication**. The responses from this endpoint do not contain any sensitive information or secrets and have been anonymized.

Its main goal is to allow users to easily diagnose issues with their Lychee installation even if they can't log in.

#### About `fopen` and DNS Rebinding

We do **not** accept vulnerability reports related to DNS rebinding attacks on `fopen` when `USE_FOPEN_FOR_URL_IMPORTS` is enabled.

We know that this setting switches URL-based imports from `curl` to PHP's native `fopen`. Unlike `curl`, `fopen` performs its own DNS resolution with no built-in mechanism to detect or prevent DNS rebinding — there is no supported way to mitigate this at the application level while still using `fopen`.

Deliberately enabling `USE_FOPEN_FOR_URL_IMPORTS` and then expecting the system to be immune to DNS rebinding is not a valid threat model. **Putting the system into a known-vulnerable configuration and then reporting the resulting exposure is not considered a security vulnerability in Lychee.**

#### Path traversal on Import From Server

We do not consider this a vulnerability because the import from server feature is only available to admin users. The path traversal issue is mitigated by the fact that only trusted users can access this functionality.

Furthermore, in the case where there are multiple admins, the owner of the Lychee installation can even disable this functionality completely by adding the following to the `.env`:

```ini
DISABLE_IMPORT_FROM_SERVER=true
```

## Core Development Discussion

You may propose new features or improvements to existing Lychee behavior in the [issue board][1]. If you propose a new feature, please be willing to implement at least some of the code that would be needed to complete the feature.

## Lychee Documentation

This site covers using and configuring Lychee. For developer-facing documentation (architecture notes, specs, and other internals), see the [`docs/` folder of the main repository](https://github.com/LycheeOrg/Lychee/tree/master/docs).

## Joining the Team

You would like to support Lychee and help us push it further? Join the team!
Your passion for Lychee will be measured by how active you are on the project.
We are looking for people who are willing to help us maintain the project,
review pull requests, and fix bugs.

If you would like to just do code review, that's fine too!
We are always looking for more eyes on the code to ensure its quality.
We will need your github handle to add you to the reviewer team.

Contact us on [Discord][2]  if you would like to know more! :)

## How to properly submit a pull-request to Lychee?

Pull requests need to respect a few simple constraints described below, which make everyone's life easier.

### Which Branch to use?
When submitting a new feature or fixing a bug, please create a new branch from master:

```bash
git checkout master
git pull master
git checkout -b <fix-issue-number>
```

Modify the desired files.

```bash
git add <files-you-modified>
git commit -m "<mesage-of-what-is-going-on>"
git push -u
```

You can then open a [pull request][4].

### Our Coding Style
In order to ease the review of pull requests, we use a uniform code style. Our Continuous Integration suite will 
fail if the latter is not respected.

#### PHP

Our coding style is defined by the `.php-ps` file; it is mostly the Symfony style, but we use tabs instead of spaces.

Before submitting a PR, we highly encourage you to do the following:
```bash
./vendor/bin/php-cs-fixer fix -v --config=.php-cs-fixer.php
# or
make formatting
```
This will ensure that the code is consistent with our style.

In order to make this less constraining, you can copy the `pre-commit` file in the root into the `.git/hooks` folder. This will execute the fixer to maintain consistency.

Our current configuration can be found [here](https://github.com/LycheeOrg/Lychee/blob/master/.php_cs).
For details about the options, you can have a look at the [php-cs-fixer-configurator](https://mlocati.github.io/php-cs-fixer-configurator)

#### Javascript/TypeScript

Similarly to what was described above, you can format the code automatically with:

```bash
npm run format
```

### PHPDoc
Below is an example of a valid Laravel documentation block. Note that the @param attribute is followed by two spaces, the argument type, two more spaces, and finally the variable name:

```php
/**
 * Register a binding with the container.
 *
 * @param  string|array  $abstract
 * @param  \Closure|string|null  $concrete
 * @param  bool  $shared
 * @return void
 *
 * @throws \Exception
 */
public function bind($abstract, $concrete = null, $shared = false)
{
    //
}
```

In addition to code documentation, we highly encourage you to use the types system of PHP for the function arguments.
This will increase the trust and safety of the code.

### Illustrated example

As follows, we present a simple pull request modifying the `readme.md`.

<ol>
<li>Fork the repository.


</li>

<li>Select your own account as the destination of the fork.


</li>

<li>Create a new branch.

<ol style="margin-left:1em">
<li>Click on the drop down menu.</li>
<li>Enter the name of the new branch.</li>
<li>Click **Create branch:...**.</li>
</ol>


</li>

<li>Make sure the new branch is selected.


</li>

<li>Edit the desired files.


</li>

<li>Commit the changes.

If possible, provide a summary of the changes done by the commit in the description before clicking **Commit changes**


</li>

<li>Create a new pull request.


</li>

<li>Fill in and submit the pull request.

<ol style="margin-left:1em">
<li>Make sure that the base repository is the one from `LycheeOrg` on the branch `master`.</li>
<li>Provide a good title to the changes you would like to be applied.</li>
<li>Add a description of the changes, motivations, what is being done etc. Also make sure that **Allow edit by the maintainers is being checked**This will allow us to fix the pull request if necessary.</li>
<li>Submit the pull request.</li>
</ol>


</li>

<li>Wait for the team to get back at you and review the PR.</li>

</ol>

## Code of Conduct

The Lychee code of conduct can be found [here](https://lycheeorg.dev/code-of-conduct/).

In summary:

- Treat everyone with respect and kindness.
- Be thoughtful in how you communicate.
- Don’t be destructive or inflammatory.
- If you encounter an issue, please contact us directly on [Discord](https://discord.gg/JMPvuRQcTf).

[1]: https://github.com/LycheeOrg/Lychee/issues
[2]: https://discord.gg/JMPvuRQcTf
[3]: https://github.com/LycheeOrg/Lychee/discussions
[4]: https://github.com/LycheeOrg/Lychee/pulls
[5]: https://github.com/LycheeOrg/Lychee/security/advisories/new

---

## File: src/content/docs/docs/getting-started/docker.md

---
title: "Docker"
sidebar:
  order: 2
---

### Overview

The recommended way to install Lychee is via Docker using the official image at `ghcr.io/lycheeorg/lychee`. The Docker image provides a complete, production-ready deployment with all dependencies included.

As of Version 7, the default Docker image uses **FrankenPHP with Laravel Octane** instead of the traditional nginx + PHP-FPM stack. This modern architecture delivers dramatic performance improvements:
- Boot time reduced from 40-60ms to 4-6ms per request
- 3-4x better throughput
- Significantly reduced latency
- Framework components kept in memory and reused across requests

### Available Tags

The following tags are available:

* `latest`: Latest stable Lychee release using FrankenPHP
* `v[NUMBER]`: Specific stable version (e.g., `v7.0.0`) using FrankenPHP
* `edge`: Current development/bleeding edge builds using FrankenPHP
* `legacy`: Latest release using nginx + PHP-FPM (deprecated)
* `v[NUMBER]-legacy`: Specific stable version using nginx + PHP-FPM (deprecated)

**Recommendation:** Use `latest` or specific version tags for production deployments.

## Quick Start

### Docker Compose (Recommended)

The recommended way to deploy Lychee is with Docker Compose.

:::tip
Not sure which options you need? Use the [**Docker Compose Wizard**](/wizard/) to generate a ready-to-run `docker-compose.yaml` and `.env` tailored to your setup (database, workers, reverse proxy, OAuth, and more) — right in your browser, no install required.
:::

Alternatively, use the official template as a starting point and edit it by hand:

[https://github.com/LycheeOrg/Lychee/blob/master/docker-compose.yaml](https://github.com/LycheeOrg/Lychee/blob/master/docker-compose.yaml)

Start the services:

```bash
docker-compose up -d
```

## Volume Mounts

### Version 7+ (FrankenPHP)

The current volume structure for Version 7 and later:

```yaml
volumes:
  - ./lychee/uploads:/app/public/uploads           # Photo storage
  - ./lychee/storage/app:/app/storage/app          # Application storage
  - ./lychee/logs:/app/storage/logs                # Log files
  - ./lychee/tmp:/app/storage/tmp                  # Temporary files
  - ./conf/user.css:/app/public/dist/user.css      # Optional: Custom CSS
  - ./conf/custom.js:/app/public/dist/custom.js    # Optional: Custom JavaScript
```

### Legacy (nginx + PHP-FPM)

If using the legacy image:

```yaml
volumes:
  - ./lychee/conf:/conf       # Configuration
  - ./lychee/uploads:/uploads # Photo storage
  - ./lychee/sym:/sym         # Symbolic links
  - ./lychee/logs:/logs       # Log files
  - ./lychee/tmp:/lychee-tmp  # Temporary files
```

**Important:** If you're upgrading from Version 6 to Version 7, you must update your volume mounts. See the [upgrade documentation](/docs/administration/upgrade/) for migration steps.

## Environment Variables

### Database Configuration

Configure your database connection:

```yaml
environment:
  - DB_CONNECTION=mysql           # mysql, pgsql, or sqlite
  - DB_HOST=lychee_db            # Database hostname
  - DB_PORT=3306                 # Database port
  - DB_DATABASE=lychee           # Database name
  - DB_USERNAME=lychee           # Database username
  - DB_PASSWORD=lychee           # Database password
```

### Basic Configuration

Common environment variables:

```yaml
environment:
  - APP_URL=http://localhost:8000  # Your public URL
  - APP_DEBUG=false                # Enable debug mode (development only)
  - APP_KEY=base64:YOUR_APP_KEY_HERE  # Application encryption key (generate with `php artisan key:generate --show` or use `openssl rand -base64 32`)
  - TIMEZONE=UTC                   # Server timezone
  - LOG_CHANNEL=stack              # Logging channel
```

### Access Rights Configuration

```yaml
environment:
  - PUID=1000          # User ID for file permissions
  - PGID=1000          # Group ID for file permissions
  - PHP_TZ=UTC         # PHP timezone
  #- RUN_AS_ROOT=yes    # Run PHP processes as root (yes/no), disabled by default for security
```

## Advanced Features

### Worker Mode for Horizontal Scaling (Recommended)

Version 7 introduces worker mode for processing background jobs independently. This enables horizontal scaling for improved performance with large photo uploads and processing tasks.

#### Basic Setup (Without Workers)

The basic single-service setup handles both web requests and background jobs.
However, the requests are limited to 30s by default, which may not be sufficient for large uploads or processing.

```yaml
services:
  lychee:
    image: ghcr.io/lycheeorg/lychee:latest
    # ... volumes, environment, etc.
```

#### Advanced Setup (With Workers)

For better performance, run dedicated worker services:

```yaml
services:
  lychee_api:
    image: ghcr.io/lycheeorg/lychee:latest
    container_name: lychee
    ports:
      - "8000:8000"
    volumes:
      - ./lychee/uploads:/app/public/uploads
      - ./lychee/storage/app:/app/storage/app
      - ./lychee/logs:/app/storage/logs
      - ./lychee/tmp:/app/storage/tmp
      - ./lychee/conf/.env:/app/.env:ro
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=lychee_db
      - QUEUE_CONNECTION=database  # CRITICAL: Enable queue processing
      # ... other environment variables
    depends_on:
      - lychee_db
    networks:
      - lychee

  lychee_worker:
    image: ghcr.io/lycheeorg/lychee:latest
    container_name: lychee_worker
    volumes:
      - ./lychee/uploads:/app/public/uploads
      - ./lychee/storage/app:/app/storage/app
      - ./lychee/logs:/app/storage/logs
      - ./lychee/tmp:/app/storage/tmp
      - ./lychee/conf/.env:/app/.env:ro
    environment:
      - LYCHEE_MODE=worker         # CRITICAL: Run in worker mode
      - DB_CONNECTION=mysql
      - DB_HOST=lychee_db
      - DB_PORT=3306
      - DB_DATABASE=lychee
      - DB_USERNAME=lychee
      - DB_PASSWORD=lychee
      - QUEUE_CONNECTION=database  # CRITICAL: Must match API service
      # ... other environment variables
    depends_on:
      - lychee_db
      - lychee_api
    networks:
      - lychee
```

**Critical Requirements for Worker Mode:**
1. Set `QUEUE_CONNECTION=database` (or `redis`) in **both** API and worker services
2. Set `LYCHEE_MODE=worker` in worker service only
3. Ensure both services share the same database and volume mounts

#### Scaling Workers

Run multiple worker instances for parallel processing:

**Option 1: Using replicas (Docker Swarm/Compose v3)**

```yaml
lychee_worker:
  image: ghcr.io/lycheeorg/lychee:latest
  deploy:
    replicas: 3  # Run 3 worker instances
  # ... rest of configuration
```

**Option 2: Multiple named services**

```yaml
lychee_worker_1:
  image: ghcr.io/lycheeorg/lychee:latest
  container_name: lychee_worker_1
  environment:
    - LYCHEE_MODE=worker
  # ... rest of configuration

lychee_worker_2:
  image: ghcr.io/lycheeorg/lychee:latest
  container_name: lychee_worker_2
  environment:
    - LYCHEE_MODE=worker
  # ... rest of configuration
```

### Docker Secrets

For sensitive information, use Docker secrets instead of environment variables:

```yaml
services:
  lychee:
    image: ghcr.io/lycheeorg/lychee:latest
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password
      - APP_KEY_FILE=/run/secrets/app_key
      # - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      # - MAIL_PASSWORD_FILE=/run/secrets/mail_password
      - ADMIN_PASSWORD_FILE=/run/secrets/admin_password
    secrets:
      - db_password
      - app_key
      # - redis_password
      # - mail_password
      - admin_password

secrets:
  app_key:
    file: ./secrets/app_key.txt
  db_password:
    file: ./secrets/db_password.txt
  # redis_password:
  #   file: ./secrets/redis_password.txt
  # mail_password:
  #   file: ./secrets/mail_password.txt
  admin_password:
    file: ./secrets/admin_password.txt
```

Supported `_FILE` variables:
- `APP_KEY_FILE`
- `DB_PASSWORD_FILE`


- `ADMIN_PASSWORD_FILE`

## Configuration Management

### Environment File (.env)

**Important:** Due to the FrankenPHP architecture, the `.env` file is read once at container startup and kept in memory. Any changes to the `.env` file require a container restart to take effect:

```bash
docker-compose restart lychee
```

### Configuration Priority

Configuration values are applied in this order (highest to lowest priority):
1. Environment variables passed via `docker-compose.yml` or `docker run`
2. Values in mounted `.env` file
3. Default values from `.env.example`

**Note:** When restarting containers, environment variables from docker-compose will override values in the mounted `.env` file. Modify configuration in your docker-compose file rather than editing files inside the container.

## Upgrading

### From Version 6 to Version 7

Version 7 introduces breaking changes to the Docker setup. **You must update your docker-compose configuration** when upgrading from v6 to v7. Simply changing the image tag will not work.

See the detailed [upgrade documentation](/docs/administration/upgrade/#upgrading-lychee-docker-installations-from-v6-to-v7) for complete migration instructions, including:
- Volume mount changes
- Service architecture updates
- Environment variable changes
- Worker mode configuration

### General Upgrade Process

For routine updates:

1. **Backup your data:**
   ```bash
   # Backup database
   docker exec lychee_db mysqldump -u lychee -p lychee > lychee_backup.sql

   # Backup uploads
   cp -r ./lychee ./lychee_backup
   ```

2. **Pull the latest image:**
   ```bash
   docker-compose pull
   ```

3. **Restart services:**
   ```bash
   docker-compose down
   docker-compose up -d
   ```

4. **Run migrations:**
   ```bash
   docker exec lychee php artisan migrate
   ```

## Troubleshooting

### Check Service Status

```bash
docker-compose ps
```

### View Logs

```bash
# All services
docker-compose logs -f

# Specific service
docker-compose logs -f lychee

# Worker logs
docker-compose logs -f lychee_worker
```

### Common Issues

**Workers not processing jobs:**
- Verify `QUEUE_CONNECTION=database` is set in both API and worker services
- Verify `LYCHEE_MODE=worker` is set in worker service
- Check worker logs: `docker-compose logs -f lychee_worker`

**Upload issues:**
- Verify volume mounts point to correct paths
- Check file permissions on host directories
- Ensure uploads directory exists and is writable

**Performance issues:**
- Consider adding worker services for background processing
- Verify FrankenPHP is running (check logs for FrankenPHP, not nginx)
- Ensure `QUEUE_CONNECTION` is set for async job processing

**Database connection errors:**
- Ensure database service name matches `DB_HOST` value
- Verify database credentials are correct
- Check database service is healthy: `docker-compose ps lychee_db`

**Configuration changes not applying:**
- Remember to restart container after `.env` changes: `docker-compose restart lychee`
- Verify environment variables in docker-compose.yml take precedence over `.env` file

### Getting Help

For additional support:
- [GitHub Discussions](https://github.com/LycheeOrg/Lychee/discussions)
- [Discord Server](https://discord.gg/y4aUbnF)
- [FAQ](/docs/faq/troubleshooting/)

## Performance Optimization

### Recommended Setup for Large Galleries

For galleries with thousands of photos:

1. **Use worker services** for background processing
2. **Scale workers** based on your hardware (2-4 workers recommended)
3. **Use Redis** for queue backend instead of database:
   ```yaml
   environment:
     - QUEUE_CONNECTION=redis
     - REDIS_HOST=redis
     - REDIS_PORT=6379
   ```
4. **Allocate sufficient resources** in docker-compose:
   ```yaml
   deploy:
     resources:
       limits:
         memory: 2G
       reservations:
         memory: 1G
   ```

### FrankenPHP Benefits

The FrankenPHP-powered Version 7 image provides significant performance improvements over Version 6:

- **Faster boot times:** 4-6ms vs 40-60ms per request
- **Better throughput:** 3-4x improvement in requests per second
- **Lower latency:** Reduced response times across all operations
- **Memory efficiency:** Framework components kept in memory and reused

These improvements are automatic when using Version 7 - no additional configuration required.

## Security Considerations

1. **Use Docker secrets** for sensitive credentials in production
2. **Mount `.env` as read-only** (`:ro`) to prevent modifications
3. **Use strong passwords** for database and admin accounts
4. **Keep images updated** to receive security patches
5. **Run behind a reverse proxy** (nginx, Traefik, Caddy) with TLS
6. **Restrict network access** using Docker networks
7. **Use specific version tags** instead of `latest` in production for reproducible deployments

---

## File: src/content/docs/docs/getting-started/installation.md

---
title: "Installation"
sidebar:
  order: 1
---

The fastest way to get Lychee running is with **Docker Compose**. A single command gets you a fully working gallery with a database, ready to use.

## Quick Start

Download the minimal compose file and start Lychee:

```bash
curl -O https://raw.githubusercontent.com/LycheeOrg/Lychee/master/docker-compose.minimal.yaml
docker compose -f docker-compose.minimal.yaml up -d
```

Lychee is now running at [http://localhost:8000](http://localhost:8000).

On first visit, you will be prompted to create an admin account.

:::tip
For a more complete setup with custom volumes, environment variables, and worker services, use the [Docker Compose Wizard](/wizard/) to generate a tailored `docker-compose.yaml` and `.env`, or see the [Docker documentation](/docs/getting-started/docker/).
:::

## What's Included

The minimal compose file sets up:

- **Lychee** with FrankenPHP (the modern, high-performance runtime)
- **MariaDB** as the database
- Persistent storage for your photos and database

## Next Steps

Once Lychee is running:

1. **Create your admin account** on first visit
2. **Upload your first photos** via the web interface
3. **Configure your instance** — see [Configuration](/docs/getting-started/configuration/) and [Settings](/docs/getting-started/settings/)

For production deployments, you should:

- Set a proper `APP_URL` and `APP_KEY` — see [Configuration](/docs/getting-started/configuration/)
- Put Lychee behind a reverse proxy with TLS (nginx, Traefik, or Caddy)
- Use the [Docker Compose Wizard](/wizard/) or the [full Docker Compose template](https://github.com/LycheeOrg/Lychee/blob/master/docker-compose.yaml) with workers for better performance

## Alternative: Manual Installation

If you cannot use Docker, Lychee can be installed manually on a server with PHP, a web server, and a database. See the [Manual Installation](/docs/getting-started/manual-installation/) guide.

---

## File: src/content/docs/docs/getting-started/manual-installation.md

---
title: "Manual Installation"
sidebar:
  order: 5
---

This guide covers installing Lychee without Docker. For most users, the [Docker installation](/docs/getting-started/installation/) is simpler and recommended.

## Server Requirements

- A web server such as Apache or nginx
- A database — using one of the following:
  - MySQL _(version > 5.7.8)_ / MariaDB _(version > 10.2)_
  - PostgreSQL _(version > 9.2)_
  - SQLite3
- PHP >= 8.4 with these extensions:
  - bcmath, ctype, dom, exif, fileinfo, filter, gd, json, libxml, ldap (optional), mbstring, openssl, pcre, PDO, Phar, SimpleXML, tokenizer, xml, xmlwriter
  - imagick _(optional — better thumbnails)_
  - [PhpRedis](https://github.com/phpredis/phpredis) _(optional — to use Redis)_
- A database-specific PHP extension:
  - SQLite3 for SQLite databases
  - MySQLi (or PDO_MySQL) for MySQL / MariaDB
  - PgSql (or PDO_PGSQL) for PostgreSQL
- On FreeBSD, you may also need: session, zlib
- Optional CLI tools:
  - Exiftool (better EXIF metadata handling)
  - FFmpeg (video thumbnails)

While Lychee works on 32-bit systems, a **64-bit OS** is strongly recommended.

## Installing Lychee

### From a Release

1. Download the latest release zip from the [Releases](https://github.com/LycheeOrg/Lychee/releases) page.
2. Extract it to your web directory (e.g. `/var/www/html`).

### From Source

Building from source requires [Composer](https://getcomposer.org/) and [npm](https://nodejs.org/).

```bash
git clone https://www.github.com/LycheeOrg/Lychee /var/www/html/Lychee
cd /var/www/html/Lychee
composer install --no-dev
npm install
npm run build
```

## Configuration

### Directory Permissions

Three OS users may be involved:

- **Web user** — the user running the web server daemon (e.g. `www-data`, `apache`, `nginx`)
- **PHP user** — the user running PHP-FPM/CGI (often the same as the web user)
- **CLI user** — your shell user, relevant if importing photos via CLI

Required permissions:

- All Lychee files must be **readable** by the web/PHP user.
- `storage/` and `bootstrap/cache/` must be **writable** by the web/PHP user.
- `public/uploads` and `public/sym` must be **writable** by the web/PHP user (and the CLI user if importing via CLI).
- `public/dist` must be **writable** if you want to customize CSS via the web frontend.

For CLI imports, the recommended approach is to add all users to a shared group, set group ownership, enable group write, and set the `sgid` bit on directories so new files inherit the group.

### Environment File (.env)

The main directory should contain a `.env` file. Composer typically creates this from `.env.example` during installation. If it doesn't exist, copy it manually:

```bash
cp .env.example .env
```

:::caution
The `.env` file contains database credentials and encryption keys. Do not expose it via your web server.
:::

### Application Key

The `APP_KEY` setting must be set. If Composer didn't generate one:

```bash
php artisan key:generate
```

### URL

Set `APP_URL` in `.env` to the external URL by which the `public/` directory is accessible. This must match your web server configuration.

### Additional Configuration

The default `.env` provides usable settings out of the box. You may want to configure cache, database, and session settings — see the [Configuration](/docs/getting-started/configuration/) page.

## Web Server Configuration

Configure your web server to serve the `public/` directory. **Do not expose the top-level Lychee directory** — this would allow public access to the `.env` file and application code.

It is strongly recommended to serve Lychee over TLS. Consider [Let's Encrypt](https://letsencrypt.org/) for certificates and [Mozilla's SSL Configuration Generator](https://ssl-config.mozilla.org/) for server configuration.

### Apache

Enable `mod_rewrite` so the included `public/.htaccess` is honored. Also check the Apache [upgrade instructions](/docs/administration/upgrade/) for required permissions in your site configuration.

If the default `.htaccess` does not work, try this alternative:

```apache
Options +FollowSymLinks -Indexes
RewriteEngine On

RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
```

### nginx

A sample nginx server block (without TLS):

```nginx
server {
    listen 80;
    server_name <mydomain>.<tld>;

    root /var/www/Lychee/public/;
    index index.php;

    # If the request is not for a valid file, send to bootstrap
    if (!-e $request_filename)
    {
        rewrite ^/(.*)$ /index.php?/$1 last;
        break;
    }

    # Serve /index.php through PHP
    location = /index.php {
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        fastcgi_param HTTP_PROXY "";
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PHP_VALUE "post_max_size=100M
            max_execution_time=200
            upload_max_filesize=30M
            memory_limit=300M";
        fastcgi_param PATH /usr/local/bin:/usr/bin:/bin;
        include fastcgi_params;
    }

    # Deny access to other .php files
    location ~ [^/]\.php(/|$) {
        return 403;
    }

    error_log  /var/log/nginx/lychee.error.log;
    access_log /var/log/nginx/lychee.access.log;
}
```

For subdirectory hosting, see the [FAQ](/docs/faq/installation/).