🐋 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 proddeploy/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 samelibrephotos.envasdocker-compose.yml, andapi/tests/infra/test_compose_files_in_sync.pykeeps the backend environment of the two files equaldeploy/compose/docker-compose.e2e.yml- End-to-end test stack (builds from local source)deploy/docker/backend/- Backend Dockerfile (app layer) andentrypoint.sh, the one entrypoint of the backend and unified images. Image differences are guarded blocks in it (SERVE_FRONTENDfor unified) orENVdefaults in a Dockerfile;api/tests/infra/test_deploy_images.pyfails on a second copydeploy/docker/backend/base/- Backend base image Dockerfile (system dependencies, including the Vulkan loader and Mesa's Vulkan drivers for the GPU), published separatelydeploy/docker/frontend/- Frontend Dockerfiles (production and development)deploy/docker/proxy/- Nginx reverse proxy configurationdeploy/docker/unified/- Single-container image (Single Container Deployment) with its own standalone Dockerfile; build it locally from the repo root withdocker build -f deploy/docker/unified/Dockerfile .
✨ Code Standards
When modifying Docker files, please ensure:
- Cross-platform compatibility: Test on both Linux and macOS/Windows (WSL2)
- ARM64 support: Images should build on both AMD64 and ARM64 (Apple Silicon, Raspberry Pi)
- Layer optimization: Order commands to maximize Docker layer caching
- 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
| File | Purpose |
|---|---|
docker-compose.yml | Base configuration, used in both dev and prod |
docker-compose.dev.yml | Development 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-slimbuildsdist/, thenhalverneus/static-file-serverserves 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:
| Variable | Purpose | Default |
|---|---|---|
tag | Docker image tag to use | latest |
httpPort | Host port for the application | 3000 |
scanDirectory | Path to photo library | - |
data | Path 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:
- 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) - 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 - Check all services:
docker compose -f docker-compose.yml -f docker-compose.dev.yml ps(all should be "healthy" or "running") - Test basic functionality: Upload a photo, trigger a scan, verify it works
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
.dockerignoreto exclude unnecessary files
Build Performance
- Order Dockerfile commands from least to most frequently changed
- Copy dependency files before source code for better caching
- Use
--parallelflag 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