Serica

Lightweight self-hosted gallery server for photo and video collections
This project is no more under active development.
This may affect its usage in unpredictable and unguaranteed ways.

About

Serica is a lightweight self-hosted gallery server built on Flask and Waitress. It serves directories of images and short videos as styled, grid-based gallery pages — no database, no authentication layer, no admin panel.

The use case is intentionally narrow: you mount a directory of media files into the container, optionally drop in a cover.jpg per subdirectory, and Serica renders one page per subdirectory plus an index of all available galleries.

Each page lays media out in a repeating 2 / 2 / 1 grid pattern (two split rows followed by a fullscreen one) and exposes a sticky navigation bar that links to all sibling galleries.

It was built to host wedding and trip photo collections behind a reverse proxy on a personal server, where the deployer controls who can reach the URL. There is no built-in authentication: if you expose Serica directly to the public internet, every gallery is world-readable.

Run it behind nginx (or any reverse proxy) with HTTP basic auth, OAuth2-proxy, or IP allowlisting if that's not what you want.

Installation

Docker

The recommended way to run Serica is via Docker, pulling the prebuilt image from GHCR:

docker pull ghcr.io/streambinder/serica:latest

The published image targets linux/arm64 only. On other architectures, build locally from the Dockerfile shipped at the repository root.

Mount your media directory at /data and your branding assets (logo, favicon) at /branding:

docker run -d \
  --name serica \
  -p 5000:5000 \
  -v /path/to/photos:/data:ro \
  -v /path/to/branding:/branding:ro \
  -e TITLE="My Gallery" \
  ghcr.io/streambinder/serica:latest

The container runs as nobody and exposes port 5000, with a healthcheck pinging /health every 30 seconds.

Directory layout

Serica expects the following layout under the volumes:

/data/
  trip-iceland/
    cover.jpg
    IMG_001.jpg
    IMG_002.jpg
    clip.mp4
  wedding/
    cover.jpg
    photo-001.png
    photo-002.png

/branding/
  logo.png
  favicon.png

Each subdirectory under /data becomes one gallery page reachable at /<gallery-name>. The optional cover.jpg inside a gallery is used as that gallery's cover background; if missing, the gallery still renders without a cover. Files with extensions outside .jpg, .jpeg, .png, .mp4, .mov are ignored.

Serica fetches logo.png and favicon.png from /branding. Neither is strictly required — if missing, the corresponding <img> / <link> tag will just 404 — but galleries look unfinished without them.

Configuration

Serica reads its configuration from a single environment variable:

VariableRequiredDefaultPurpose
TITLEno"Gallery"Site title used in the HTML <title> of the index page and each gallery ({gallery} — {TITLE})

Custom build

Serica is a single-file Flask app, so the usual Python toolchain works. Prerequisites: Python >=3.13 and uv.

git clone https://github.com/streambinder/serica.git
cd serica
uv sync --frozen
uv run waitress-serve --host 0.0.0.0 --port 5000 app:app

Note: the app hard-codes /data and /branding as absolute paths. To run outside Docker, create those directories at the filesystem root (or patch DATA_DIR and BRANDING_DIR in app.py).