Photoserv is an application for photographers, artists, or similar who want a system to act as a single source of truth for their publicly published photos.
Looking to contribute or inspect the code? See CONTRIBUTING.md.
![]() |
![]() |
|---|---|
![]() |
![]() |
- Upload and categorize photos by albums and tags.
- Extract metadata from photos for consumption in other systems.
- Exposes a REST API for applications and integrations to interact with your data.
- For example, a photo portfolio website in Astro.js can consume this.
- Swagger API browser included.
- Define multiple sizes for your photos to be available in.
- OIDC and simple auth optional.
- Web request dispatch upon global changes.
- Python plugin system for advanced integrations.
- Configure (below)
docker compose up -d
Configure the environment variables; cp example.env .env
# openssl rand -hex 64
APP_KEY=""
DEBUG_MODE=false # always false in production
TIME_ZONE=America/New_York
DATABASE_ENGINE=postgres # postgres or sqlite
DATABASE_USER=photoserv
DATABASE_PASSWORD=photoserv
DATABASE_NAME=photoserv
DATABASE_HOST=database
DATABASE_PORT=5432
REDIS_HOST=redis
REDIS_PORT=6379
ALLOWED_HOSTS=127.0.0.1,localhost # Add photoserv domain here
SIMPLE_AUTH=True # Recommended to disable if you use OIDC
# Each of OIDC_CLIENT_*, OIDC_*_ENDPOINT must be filled to enable OIDC
OIDC_NAME=Single Sign On Button Label
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_AUTHORIZATION_ENDPOINT=
OIDC_TOKEN_ENDPOINT=
OIDC_USER_ENDPOINT=
OIDC_JWKS_ENDPOINT=
OIDC_SIGN_ALGO=RS256 # optionalOIDC Callback URL: <your-photoserv-root>/login/oidc/callback/
Example: https://photoserv.domain.com/login/oidc/callback/
Important
Be sure to set an OIDC Access Token expiration that is long enough for the duration of time you may be working on the multi-photo upload form. I use 1 hour.
Once set up, visit https://<your-instance/swagger for an interactive Swagger API browser.
Note
You will have to create an API key from within Photoserv (Settings > Public API) before
using Swagger.
While I've made my best effort to secure this application, leaning on existing solutions and libraries where possible, I am one person, and I cannot guarantee it is perfect. It is not recommended to expose this application directly to the internet. Ideally:
- Run this application by a reverse proxy (ports are commented out by default in
docker-compose.yml). - Use a tunnel or on-prem runner to build derivative websites off of the API.
- If necessary, try to only expose the public api (
/api). - Otherwise, put the frontend in front of a proxy-auth middleware like Authentik.
Assume:
websecure- internal HTTPSwebsecure-externalexternal access HTTPS
- "traefik.http.routers.photoserv.rule=Host(`photoserv.domain.com`)"
- "traefik.http.routers.photoserv.entrypoints=websecure"
- "traefik.http.services.photoserv.loadbalancer.server.port=8000"
- "traefik.http.routers.photoserv.rule=Host(`photoserv.domain.com`)"
- "traefik.http.routers.photoserv.entrypoints=websecure"
- "traefik.http.routers.photoserv.service=photoserv"
- "traefik.http.routers.photoserv-external.rule=Host(`photoserv.domain.com`) && PathPrefix(`/api`)"
- "traefik.http.routers.photoserv-external.entrypoints=websecure,websecure-external"
- "traefik.http.routers.photoserv-external.service=photoserv"
- "traefik.http.services.photoserv.loadbalancer.server.port=8000"
- "traefik.http.routers.photoserv.rule=Host(`photoserv.domain.com`) && PathPrefix(`/api`)"
- "traefik.http.routers.photoserv-external.entrypoints=websecure,websecure-external"
- "traefik.http.routers.photoserv.service=photoserv"
- "traefik.http.routers.photoserv-external.rule=Host(`photoserv.domain.com`)"
- "traefik.http.routers.photoserv-external.entrypoints=websecure-external"
- "traefik.http.routers.photoserv-external.middlewares=authentik@docker"
- "traefik.http.routers.photoserv-external.service=photoserv"
- "traefik.http.services.photoserv.loadbalancer.server.port=8000"
Photoserv can be configured to dispatch web requests upon a global change. This can be useful for triggering a static site generator upon creating content.
Web requests will be dispatched 10 minutes after the most recent Photoserv change to reduce excessive dispatches.
Suppose you have a SSG project set up in a Gitea repo. You can call the deploy.yml workflow using Photoserv like so:
| Parameter | Value |
|---|---|
| Method | POST |
| URL | https://your-gitea-instance.domain/api/v1/repos/<namespace>/<repo>/actions/workflows/deploy.yml/dispatches |
| Headers | Content-Type: application/jsonAuthorization: token ${GITEA_KEY} |
| Body | { "ref": "main" } |
| Active | Checked |
Photoserv supports Python-based plugins to extend functionality beyond web requests. Plugins can intercept global changes as well as photo publish events. Use plugins for things like social media integration or more complex workflows.
See the Photoserv plugin repository for first-class plugins, examples, and documentation. Be careful running Python plugins as they essentially allow arbitrary code execution.
Both web requests and plugins can use environment variables in the ${ENV_VAR} format to safely reference secrets. Be careful not to leak secrets with this!
- Astro Loader - For the Astro static site generator.
AI has been used in the capacity of an advanced autocomplete while making this project. All architectural choices and model interfaces have been created and decided upon by a human, with physical pen-and-paper, or while on a long run. This entire README is handwritten without obnoxious emojis.





