# LumaVault

LumaVault is a self-hosted photo and video library for organizing, browsing, and sharing personal media. It keeps original files on storage you control while providing a fast web interface, automatic metadata extraction, albums, search, and optional machine-learning features.

## Features

- Responsive web interface for desktop and mobile
- Photo and video uploads with resumable transfers
- Automatic thumbnail and preview generation
- Timeline grouped by capture date
- Search by filename, date, camera, location, and tags
- Albums, favorites, archive, and trash
- EXIF and video metadata extraction
- Duplicate detection using file hashes
- Multiple user accounts with private libraries
- Read-only public album sharing
- Optional face recognition and semantic search
- Background library scanning for existing folders
- Original files preserved without modification
- PostgreSQL-backed metadata and Redis job queue
- Docker-based installation and upgrades

## Requirements

- Docker Engine 24 or newer
- Docker Compose v2
- At least 2 GB of RAM
- A writable directory for uploads and generated previews
- PostgreSQL 15 or newer when running without Docker

Machine-learning features require additional memory and are disabled by default. Allow at least 4 GB of RAM when enabling them.

## Installation

### Docker Compose

Create a project directory:

```bash
mkdir lumavault
cd lumavault
```

Create a `.env` file:

```dotenv
LUMAVAULT_VERSION=latest
LUMAVAULT_URL=http://localhost:8080
LUMAVAULT_SECRET=replace-with-a-long-random-string

DB_NAME=lumavault
DB_USER=lumavault
DB_PASSWORD=replace-with-a-strong-password
```

Create `compose.yaml`:

```yaml
services:
  server:
    image: ghcr.io/lumavault/lumavault:${LUMAVAULT_VERSION:-latest}
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      LUMAVAULT_URL: ${LUMAVAULT_URL}
      LUMAVAULT_SECRET: ${LUMAVAULT_SECRET}
      DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@database:5432/${DB_NAME}
      REDIS_URL: redis://redis:6379/0
      MEDIA_ROOT: /data/media
      CACHE_ROOT: /data/cache
    volumes:
      - ./data/media:/data/media
      - ./data/cache:/data/cache
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_started

  worker:
    image: ghcr.io/lumavault/lumavault:${LUMAVAULT_VERSION:-latest}
    restart: unless-stopped
    command: worker
    environment:
      LUMAVAULT_SECRET: ${LUMAVAULT_SECRET}
      DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@database:5432/${DB_NAME}
      REDIS_URL: redis://redis:6379/0
      MEDIA_ROOT: /data/media
      CACHE_ROOT: /data/cache
    volumes:
      - ./data/media:/data/media
      - ./data/cache:/data/cache
    depends_on:
      - server

  database:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - ./data/redis:/data
```

Start the services:

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

Open `http://localhost:8080` and create the initial administrator account. Registration is disabled automatically after the first account is created.

### Using an Existing Photo Directory

Mount an existing library as read-only and configure it as an import source:

```yaml
services:
  server:
    volumes:
      - /srv/photos:/imports/photos:ro
      - ./data/media:/data/media
      - ./data/cache:/data/cache

  worker:
    volumes:
      - /srv/photos:/imports/photos:ro
      - ./data/media:/data/media
      - ./data/cache:/data/cache
```

After restarting the services, add `/imports/photos` under **Administration → Libraries**. LumaVault indexes these files in place and does not modify them.

## Usage

### Uploading Media

Select **Upload** in the web interface, then choose files or folders. Uploaded originals are stored below `MEDIA_ROOT` using date-based directories.

Supported formats include JPEG, PNG, WebP, GIF, HEIC, TIFF, RAW camera files, MP4, MOV, and WebM. Browser preview support varies by format; LumaVault generates compatible previews when necessary.

### Creating Albums

1. Select one or more items from the timeline.
2. Choose **Add to album**.
3. Select an existing album or create a new one.
4. Optionally enable sharing from the album settings.

Removing an item from an album does not delete the original file.

### Importing Existing Files

Administrators can add import directories from **Administration → Libraries**. A scan can be started from the web interface or the command line:

```bash
docker compose exec server lumavault scan /imports/photos
```

Use `--watch` to monitor a directory continuously:

```bash
docker compose exec -d server lumavault scan --watch /imports/photos
```

### Managing Background Jobs

View thumbnail generation, metadata extraction, and indexing jobs under **Administration → Jobs**.

To retry failed jobs from the command line:

```bash
docker compose exec worker lumavault jobs retry --failed
```

### Backups

Back up the database, original media, and application secret. Generated previews can be recreated and do not need to be backed up.

```bash
docker compose exec -T database pg_dump \
  -U "$DB_USER" "$DB_NAME" > lumavault-backup.sql
```

Also copy these paths:

```text
data/media/
.env
```

To restore the database:

```bash
cat lumavault-backup.sql | docker compose exec -T database \
  psql -U "$DB_USER" "$DB_NAME"
```

## Configuration

LumaVault is configured with environment variables. Changes require restarting the affected services:

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

| Variable | Default | Description |
| --- | --- | --- |
| `LUMAVAULT_URL` | `http://localhost:8080` | Public URL used for links and redirects |
| `LUMAVAULT_SECRET` | — | Secret used to sign sessions and tokens; required |
| `DATABASE_URL` | — | PostgreSQL connection URL; required |
| `REDIS_URL` | `redis://localhost:6379/0` | Redis connection URL |
| `MEDIA_ROOT` | `/data/media` | Directory containing uploaded originals |
| `CACHE_ROOT` | `/data/cache` | Directory for thumbnails and generated previews |
| `UPLOAD_MAX_SIZE` | `20GB` | Maximum size of a single upload |
| `REGISTRATION_ENABLED` | `false` | Allow new users to register |
| `TRASH_RETENTION_DAYS` | `30` | Days before trashed items are permanently removed |
| `SCAN_INTERVAL` | `0` | Automatic scan interval in minutes; `0` disables it |
| `TIME_ZONE` | `UTC` | Time zone used when media lacks offset information |
| `LOG_LEVEL` | `info` | Logging level: `debug`, `info`, `warning`, or `error` |
| `ML_ENABLED` | `false` | Enable face recognition and semantic search |
| `ML_DEVICE` | `cpu` | Processing device: `cpu`, `cuda`, or `mps` |
| `MAP_PROVIDER` | `openstreetmap` | Provider used for map tiles |
| `SMTP_URL` | — | SMTP connection URL for email notifications |
| `SMTP_FROM` | — | Sender address for outgoing email |
| `OIDC_ISSUER_URL` | — | OpenID Connect issuer URL |
| `OIDC_CLIENT_ID` | — | OpenID Connect client ID |
| `OIDC_CLIENT_SECRET` | — | OpenID Connect client secret |

### Reverse Proxy

When exposing LumaVault publicly, place it behind a reverse proxy with HTTPS and set `LUMAVAULT_URL` to the external address:

```dotenv
LUMAVAULT_URL=https://photos.example.com
```

The proxy must forward these headers:

```text
Host
X-Forwarded-For
X-Forwarded-Proto
```

Uploads may require increasing the proxy request-body limit and timeout.

### Hardware Acceleration

To enable NVIDIA GPU processing for machine-learning jobs, set:

```dotenv
ML_ENABLED=true
ML_DEVICE=cuda
```

Then expose the GPU to the worker:

```yaml
services:
  worker:
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
```

### File Permissions

The container runs as user and group ID `1000` by default. Ensure mounted directories are writable:

```bash
sudo chown -R 1000:1000 data/media data/cache
```

Alternatively, set `PUID` and `PGID` on the `server` and `worker` services to match the owner of your media directories.

## Updating

Pull the latest images and restart the stack:

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

Database migrations run automatically during startup. Back up the database before upgrading across major versions.

## Troubleshooting

View service logs:

```bash
docker compose logs -f server worker
```

Check service status:

```bash
docker compose ps
```

If previews are missing, rebuild them:

```bash
docker compose exec worker lumavault previews rebuild --missing
```

If imported files do not appear, verify that the directory is mounted into both the `server` and `worker` containers and is readable by the configured user.

## Security

Do not expose PostgreSQL or Redis directly to the internet. Use a strong, unique `LUMAVAULT_SECRET`, terminate public traffic with HTTPS, and keep Docker images updated.

Shared albums are accessible to anyone with the link unless password protection or an expiration date is configured.

## License

LumaVault is licensed under the GNU Affero General Public License v3.0. See `LICENSE` for details.