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:
| Variable | Required | Default | Purpose |
|---|---|---|---|
TITLE | no | "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).