Skip to main content

🐋 Docker

Overview​

The deploy/ directory in the monorepo contains the Docker configuration for running LibrePhotos. This includes:

  • deploy/compose/docker-compose.yml - Base configuration (published images), used in both dev and prod
  • deploy/compose/docker-compose.dev.yml - Development overrides (builds from local source, hot reload)
  • deploy/compose/docker-compose.unified.yml - Single-container image with PostgreSQL; reads the same librephotos.env as docker-compose.yml, and api/tests/infra/test_compose_files_in_sync.py keeps the backend environment of the two files equal
  • deploy/compose/docker-compose.e2e.yml - End-to-end test stack (builds from local source)
  • deploy/docker/backend/ - Backend Dockerfile (app layer) and entrypoint.sh, the one entrypoint of the backend and unified images. Image differences are guarded blocks in it (SERVE_FRONTEND for unified) or ENV defaults in a Dockerfile; api/tests/infra/test_deploy_images.py fails on a second copy
  • deploy/docker/backend/base/ - Backend base image Dockerfile (system dependencies, including the Vulkan loader and Mesa's Vulkan drivers for the GPU), published separately
  • deploy/docker/frontend/ - Frontend Dockerfiles (production and development)
  • deploy/docker/proxy/ - Nginx reverse proxy configuration
  • deploy/docker/unified/ - Single-container image (Single Container Deployment) with its own standalone Dockerfile; build it locally from the repo root with docker build -f deploy/docker/unified/Dockerfile .

✨ Code Standards​

When modifying Docker files, please ensure:

  1. Cross-platform compatibility: Test on both Linux and macOS/Windows (WSL2)
  2. ARM64 support: Images should build on both AMD64 and ARM64 (Apple Silicon, Raspberry Pi)
  3. Layer optimization: Order commands to maximize Docker layer caching
  4. Security: Don't run services as root when avoidable; use minimal base images

Architecture​

Container Relationships​

┌─────────────┐
│ proxy │ :80
│ (nginx) │ also serves /data, /protected_media,
└──────┬──────┘ /original from its own bind mounts
│
┌──────────┴──────────┐
┌─────▼─────┐ ┌─────▼─────┐
│ frontend │ │ backend │
│ (React) │ │ (Django) │
└───────────┘ └─────┬─────┘
│
┌─────▼─────┐
│ db │
│(Postgres) │
└───────────┘

Development vs Production​

FilePurpose
docker-compose.ymlBase configuration, used in both dev and prod
docker-compose.dev.ymlDevelopment overrides (hot reload, mounted source, pgadmin on :3001)

Making Changes​

Modifying the Backend Dockerfile​

The backend Dockerfile (deploy/docker/backend/Dockerfile) builds the Django application:

cd librephotos/deploy/compose

# Test your changes
docker compose -f docker-compose.yml -f docker-compose.dev.yml build backend
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d backend

# Check logs for errors
docker compose logs -f backend

Modifying the Backend Base Image​

The backend image is built in two tiers. deploy/docker/backend/Dockerfile starts FROM reallibrephotos/librephotos-base:${BASE_TAG} (BASE_TAG defaults to dev), then pip-installs requirements.txt and copies in apps/backend/. The requirements files are copied and installed before the source, so a change that only touches Python code reuses the cached dependency layers. The only system-level dependencies are Python and perl (for the ExifTool wheel), in the separate deploy/docker/backend/base/Dockerfile; libvips, LibRaw, ExifTool, ffmpeg, libpq and OpenCV come from pip as prebuilt wheels (the ones PyPI lacks are built by .github/workflows/prebuilt-wheels.yml). There is no PyTorch: the ML models run on ONNX Runtime, which is a plain pip package in requirements.txt.

The base is pulled, not built: docker compose build backend, even with --no-cache, will not rebuild it. To test a change to the base locally, build and tag it yourself first, then rebuild the backend:

# from the monorepo root
docker build -t reallibrephotos/librephotos-base:dev deploy/docker/backend/base

# then rebuild the app layer (from deploy/compose/)
docker compose -f docker-compose.yml -f docker-compose.dev.yml build backend

The base image is published to Docker Hub by .github/workflows/image-backend-base.yml, which runs only on pushes to dev that touch deploy/docker/backend/base/**. The unified image (deploy/docker/unified/) also builds FROM this base, so a base change affects it too. A release does not rebuild the base: it promotes the current :dev to :<release-tag>, and the release builds of the backend and unified images pass BASE_TAG=<release-tag>, so a published release keeps the base it shipped with.

Modifying the Frontend Dockerfile​

The frontend has two Dockerfiles:

  • deploy/docker/frontend/Dockerfile - Production build (multi-stage: node:24-slim builds dist/, then halverneus/static-file-server serves it on port 3000 behind the nginx proxy)
  • deploy/docker/frontend/Dockerfile.dev - Development build (hot reload)

Modifying the Proxy (Nginx)​

The proxy configuration is in deploy/docker/proxy/nginx.conf. After changes:

docker compose -f docker-compose.yml -f docker-compose.dev.yml build proxy
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d proxy

Environment Variables​

Environment variables are loaded from the .env file. Key variables for Docker:

VariablePurposeDefault
tagDocker image tag to uselatest
httpPortHost port for the application3000
scanDirectoryPath to photo library-
dataPath to persistent data-

The dev overlay bind-mounts apps/backend, apps/frontend and packages (the frontend compiles the shared packages/api-client from source) by path relative to deploy/compose/, so there is no environment variable for the source-code location.

Testing Changes​

Before submitting a PR, from deploy/compose/. Include the dev overlay on every command — the base docker-compose.yml uses published images and defines no build: sections, so docker compose build on its own rebuilds nothing and starts the images pulled from Docker Hub instead of your change:

  1. Clean build: docker compose -f docker-compose.yml -f docker-compose.dev.yml build --no-cache (this does not refresh the base image — see Modifying the Backend Base Image)
  2. Fresh start: docker compose -f docker-compose.yml -f docker-compose.dev.yml down -v && docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
  3. Check all services: docker compose -f docker-compose.yml -f docker-compose.dev.yml ps (all should be "healthy" or "running")
  4. Test basic functionality: Upload a photo, trigger a scan, verify it works
note

On a fresh stack, uploads are off by default: librephotos.env ships without an allowUpload entry, so docker-compose.yml passes ALLOW_UPLOAD=false and the upload button is hidden in the UI entirely. Enable it by adding allowUpload=true to your .env before the first start, or by turning on Allow uploads under the app's Admin → Site Settings.

GPU Support​

New in 1.5.0 There is no separate GPU image. The backend and unified images ship ONNX Runtime's WebGPU plugin (onnxruntime-ep-webgpu in apps/backend/requirements.txt), and their base image installs libvulkan1 and mesa-vulkan-drivers. apps/backend/service/onnx_session.py puts every model on the GPU when the plugin finds one, and on the CPU otherwise. To try a GPU, give the backend service /dev/dri (Intel, AMD) or an NVIDIA device reservation with NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics, as Utilizing GPU Acceleration shows. The base image change only reaches a local build after you rebuild the base (see Modifying the Backend Base Image).

Up to 1.4.0, deploy/docker/backend-gpu/ built a separate NVIDIA-only image, reallibrephotos/librephotos-gpu, on CUDA 12.1.1 and Ubuntu 22.04 (Python 3.10). It is gone, and so is the Python 3.10 floor it set.

Common Issues​

Image Size​

Keep images small by:

  • Using multi-stage builds
  • Cleaning apt/pip caches in the same layer
  • Using .dockerignore to exclude unnecessary files

Build Performance​

  • Order Dockerfile commands from least to most frequently changed
  • Copy dependency files before source code for better caching
  • Use --parallel flag for multi-service builds

Volume Permissions​

If you encounter permission issues with mounted volumes on Linux:

# Check the UID/GID of files in the container
docker exec -it backend ls -la /data

# You may need to adjust ownership
sudo chown -R $(id -u):$(id -g) ./librephotos/data