Skip to main content

Development

Architecture​

Currently the standard setup is split up into four different containers. In this document the general architecture and reason for the components will be explained.

LibrePhotos also ships a single-container image (reallibrephotos/librephotos-unified, built from deploy/docker/unified/), which is the deployment the installation docs recommend for most users. In that mode there is no NGINX proxy and no separate frontend container: the frontend build is collected into Django's static files and served by WhiteNoise from Gunicorn on port 8001 (SERVE_FRONTEND=true), and the database defaults to SQLite. See Single Container Deployment for the details.

Backend​

The Backend uses Django as a Framework. Everything the generally applies to Django can be applied to LibrePhotos. If there is some feature that is in Django, but not yet in LibrePhotos, then this would be quite easy to add.

In general there are around ten processes running in the backend.

  • As a webserver and for the handling of general API calls we use uvicorn (running the Django WSGI app through a2wsgi). Scaling works here with the worker count uvicorn provides, set via the WEB_CONCURRENCY env variable.
  • For long running jobs we use django_q2 with the ORM broker. The worker pool is sized by the WORKER_CONCURRENCY env variable (set as workerConcurrency in the compose .env, see deploy/compose/librephotos.env); left unset, django-q falls back to one worker per CPU core. The "Limiting CPU and memory usage" section of Advanced docker-compose usage covers how to tune it.
  • On top of those, manage.py start_service all (run from the container entrypoint) launches a set of small HTTP sidecar services, each on its own port, and registers a django_q2 schedule (api.services.check_services) that health-checks them every minute, restarts any that died and asks a sidecar that has held a model for two minutes without a request to unload it (/unload-model), which gives the memory back without restarting the process. The authoritative list and ports live in the SERVICES dict in apps/backend/api/sidecars.py, which also holds the one HTTP client the backend calls them with (retries on a refused connection or a 503, an error status raised); it currently covers image similarity (8002), thumbnails (8003), face recognition (8005), CLIP embeddings (8006), image captioning (8007), EXIF extraction (8010), tag extraction (8011) and OCR (8012).
    • Every sidecar is built on apps/backend/service/_common.py: the Flask app, a uniform /health (status, last_request_time, model_loaded, busy), /unload-model for those that hold a model, request parsing and the loopback bind.
    • The image similarity service uses faiss and powers "similar photos" lookups. It keeps each user's index in BASE_DATA/protected_media/similarity/<user id>.npz, so a restart of the service does not lose it; a rebuild replaces that file atomically once all of its pages arrived. It is also the second hop of semantic search: a text query is first sent to the CLIP embeddings service (8006) to be turned into an embedding, which is then handed to the image similarity service (8002) to find matching photos.
    • The thumbnail service renders RAW files that have no usable embedded JPEG preview (most RAW thumbnails come from that preview, in the worker). It runs as its own process so LibRaw's memory use and crashes stay away from the API workers and the ML services.
    • The workers reach every sidecar on 127.0.0.1, not localhost: the sidecars listen on IPv4 only, and on Windows a connection to localhost tries ::1 first and waits about two seconds before falling back (api/sidecars.py).

Frontend​

Pretty simple container. In production this is just a static webserver with the build frontend project. The dev version installs the right packages with the right node versions to seamlessly work. (The single-container image has no separate frontend container; Django serves the built frontend itself.)

Proxy​

We use a reverse proxy to handle traffic from outside to the frontend and backend. We use NGINX for it. Serving actual images and videos is also done with NGINX, because it is better at it then Django. (The single-container image has no proxy; Django serves everything itself.)

Database​

The split-container setup uses PostgreSQL, which is hardcoded in apps/backend/librephotos/settings/production.py. The single-container image supports both backends, selected with the DB_BACKEND environment variable - sqlite (the default) or postgresql - configured in apps/backend/librephotos/settings/production_noproxy.py. That module imports production.py and overrides only what running without a proxy changes, so settings added to production.py reach both deployments. Backend code should therefore stay portable across both engines.