piGallery2 is a self-hosted gallery that replaces Google Photos with a directory-first, read-only web viewer for your local photo archive.
Patrik Balla built piGallery2 to host photo libraries on low-resource microcomputers like the Raspberry Pi without touching underlying storage directories. The application mounts an existing folder structure in read-only mode, converting home folders into a searchable web gallery without database ingestion. Original media files remain unaltered on disk while family members view albums from any browser.
Google Photos centers photo management on proprietary cloud storage, uploading pictures to remote servers and organizing them through automated algorithms. That model simplifies browsing, but forces users to surrender custody of raw files and pay recurring storage fees. In contrast, piGallery2 fits into a filesystem-first routine. Organizing pictures on a computer or network share by year or event renders directly in a web interface without altering original files.
The workflow trade-off balances automation against data ownership. Google Photos automatically uploads phone camera rolls and clusters faces via remote machine learning. piGallery2 omits automatic phone backups and face-tag creation, instead reading pre-existing Adobe XMP face tags, mapping GPS coordinates, and sharing password-protected folder links. Because the server mounts media folders read-only, it cannot overwrite or delete originals. Migration friction is absent: pointing the application at a directory starts it, and leaving requires only stopping the container.
Can piGallery2 replace Google Photos? It replaces the web gallery, album browsing, and link sharing aspects of Google Photos, but lacks automated mobile camera backups and automated facial recognition. Users who want background phone sync typically pair piGallery2 with Syncthing.
Does piGallery2 modify or delete my original photo files? No. The application accesses your media folders in read-only mode, so it cannot rename, edit, or delete your source files.
Can piGallery2 run on a low-power Raspberry Pi? Yes, the application is optimized for low-resource single-board computers. It serves downscaled web previews so browsing stays responsive on modest hardware.
piGallery2 fits households with organized photo libraries seeking private, browser-based viewing without cloud lock-in, provided you handle file synchronization externally. Its permissive MIT terms let users run and adapt the software on personal equipment, while active development with updates in August 2026 confirms ongoing support. If you require automatic camera-roll uploads and machine-learning search, a cloud service or heavier suite suits you better, but for lightweight viewing of local media, piGallery2 delivers dependable performance.
We started this exact setup from scratch in a clean Linux machine on Nov 3, 2026 and checked it through piGallery2's own API: every check passed. Recipes are re-checked regularly and removed if they stop working.
mkdir -p pigallery2 && cd pigallery2
curl -fsSL https://open-source.store/tools/pigallery2/compose.yml -o compose.ymltouch .envdocker compose up -dservices:
pigallery2:
image: bpatrik/pigallery2:3.5.2
restart: always
ports:
- "80:80"
environment:
- NODE_ENV=production
volumes:
- pigallery2-config:/app/data/config
- pigallery2-db:/app/data/db
- pigallery2-images:/app/data/images
- pigallery2-tmp:/app/data/tmp
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --no-check-certificate --spider http://127.0.0.1:80/heartbeat || exit 1"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pigallery2-config:
pigallery2-db:
pigallery2-images:
pigallery2-tmp:
Free IDE for Java, Kotlin, Go, TypeScript and Python. Built on IntelliJ Community, fully open source, 300+ plugins.
Show that piGallery2 is listed as an open-source alternative. Paste into your README:
Ubuntu 24.04 · Apple container, Docker 29.1.3, Compose 2.40.3+ds1-0ubuntu1~24.04.1. Images pulled in 281 s: bpatrik/pigallery2:3.5.2.
Based on the upstream setup. Official compose (pigallery2-only). Image pinned to 3.5.2; relative host paths changed to named volumes; healthcheck added.
pigallery2-1 | 2026-11-03T12:29:47.863148836Z at injectAppVersion (/app/src/backend/middlewares/VersionMWs.js:18:13)
pigallery2-1 | 2026-11-03T12:29:47.863149502Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.863150127Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.863150752Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.863151377Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12) {
pigallery2-1 | 2026-11-03T12:29:47.863152002Z statusCode: 400,
pigallery2-1 | 2026-11-03T12:29:47.863152586Z status: 400
pigallery2-1 | 2026-11-03T12:29:47.863153169Z }
pigallery2-1 | 2026-11-03T12:29:47.863161252Z 11/3/2026, 12:29:47 PM[ERROR] Unexpected error:
pigallery2-1 | 2026-11-03T12:29:47.863170044Z URIError: Failed to decode param '%c0'
pigallery2-1 | 2026-11-03T12:29:47.863170877Z at decodeURIComponent (<anonymous>)
pigallery2-1 | 2026-11-03T12:29:47.863171544Z at decode_param (/app/node_modules/express/lib/router/layer.js:172:12)
pigallery2-1 | 2026-11-03T12:29:47.863172211Z at Layer.match (/app/node_modules/express/lib/router/layer.js:148:15)
pigallery2-1 | 2026-11-03T12:29:47.863172836Z at matchLayer (/app/node_modules/express/lib/router/index.js:585:18)
pigallery2-1 | 2026-11-03T12:29:47.863173502Z at next (/app/node_modules/express/lib/router/index.js:226:15)
pigallery2-1 | 2026-11-03T12:29:47.863174127Z at addTPl (/app/src/backend/routes/PublicRouter.js:72:20)
pigallery2-1 | 2026-11-03T12:29:47.863174752Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.863175419Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.863176044Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.863196336Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12)
pigallery2-1 | 2026-11-03T12:29:47.863197544Z at next (/app/node_modules/express/lib/router/index.js:280:10)
pigallery2-1 | 2026-11-03T12:29:47.863198127Z at injectAppVersion (/app/src/backend/middlewares/VersionMWs.js:18:13)
pigallery2-1 | 2026-11-03T12:29:47.863198627Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.863199211Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.863199711Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.863200252Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12) {
pigallery2-1 | 2026-11-03T12:29:47.863200752Z statusCode: 400,
pigallery2-1 | 2026-11-03T12:29:47.863201252Z status: 400
pigallery2-1 | 2026-11-03T12:29:47.863201752Z }
pigallery2-1 | 2026-11-03T12:29:47.863202169Z 11/3/2026, 12:29:47 PM[ERROR] ErrorDTO {
pigallery2-1 | 2026-11-03T12:29:47.863202752Z code: 36,
pigallery2-1 | 2026-11-03T12:29:47.863203211Z message: 'Unknown server side error',
pigallery2-1 | 2026-11-03T12:29:47.863203711Z details: URIError: Failed to decode param '%c0'
pigallery2-1 | 2026-11-03T12:29:47.863204252Z at decodeURIComponent (<anonymous>)
pigallery2-1 | 2026-11-03T12:29:47.863204752Z at decode_param (/app/node_modules/express/lib/router/layer.js:172:12)
pigallery2-1 | 2026-11-03T12:29:47.863205294Z at Layer.match (/app/node_modules/express/lib/router/layer.js:148:15)
pigallery2-1 | 2026-11-03T12:29:47.863205794Z at matchLayer (/app/node_modules/express/lib/router/index.js:585:18)
pigallery2-1 | 2026-11-03T12:29:47.863206294Z at next (/app/node_modules/express/lib/router/index.js:226:15)
pigallery2-1 | 2026-11-03T12:29:47.863206836Z at addTPl (/app/src/backend/routes/PublicRouter.js:72:20)
pigallery2-1 | 2026-11-03T12:29:47.863207377Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.863207919Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.863208752Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.863209252Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12)
pigallery2-1 | 2026-11-03T12:29:47.863209794Z at next (/app/node_modules/express/lib/router/index.js:280:10)
pigallery2-1 | 2026-11-03T12:29:47.863210294Z at injectAppVersion (/app/src/backend/middlewares/VersionMWs.js:18:13)
pigallery2-1 | 2026-11-03T12:29:47.863210836Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.863211336Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.863211877Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.863212377Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12) {
pigallery2-1 | 2026-11-03T12:29:47.863214294Z statusCode: 400,
pigallery2-1 | 2026-11-03T12:29:47.863214836Z status: 400
pigallery2-1 | 2026-11-03T12:29:47.863215336Z },
pigallery2-1 | 2026-11-03T12:29:47.863215794Z request: { method: 'GET', url: '/%c0' }
pigallery2-1 | 2026-11-03T12:29:47.863216336Z }
pigallery2-1 | 2026-11-03T12:29:47.865005377Z 11/3/2026, 12:29:47 PM[ERROR] Unexpected error:
pigallery2-1 | 2026-11-03T12:29:47.865016752Z URIError: Failed to decode param '%c0'
pigallery2-1 | 2026-11-03T12:29:47.865017836Z at decodeURIComponent (<anonymous>)
pigallery2-1 | 2026-11-03T12:29:47.865018586Z at decode_param (/app/node_modules/express/lib/router/layer.js:172:12)
pigallery2-1 | 2026-11-03T12:29:47.865019336Z at Layer.match (/app/node_modules/express/lib/router/layer.js:148:15)
pigallery2-1 | 2026-11-03T12:29:47.865019919Z at matchLayer (/app/node_modules/express/lib/router/index.js:585:18)
pigallery2-1 | 2026-11-03T12:29:47.865020461Z at next (/app/node_modules/express/lib/router/index.js:226:15)
pigallery2-1 | 2026-11-03T12:29:47.865021002Z at addTPl (/app/src/backend/routes/PublicRouter.js:72:20)
pigallery2-1 | 2026-11-03T12:29:47.865021711Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.865022294Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.865022836Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.865023461Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12)
pigallery2-1 | 2026-11-03T12:29:47.865024044Z at next (/app/node_modules/express/lib/router/index.js:280:10)
pigallery2-1 | 2026-11-03T12:29:47.865024544Z at injectAppVersion (/app/src/backend/middlewares/VersionMWs.js:18:13)
pigallery2-1 | 2026-11-03T12:29:47.865025086Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.865025794Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.865026336Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.865026877Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12) {
pigallery2-1 | 2026-11-03T12:29:47.865027419Z statusCode: 400,
pigallery2-1 | 2026-11-03T12:29:47.865027919Z status: 400
pigallery2-1 | 2026-11-03T12:29:47.865028419Z }
pigallery2-1 | 2026-11-03T12:29:47.865050044Z URIError: Failed to decode param '%c0'
pigallery2-1 | 2026-11-03T12:29:47.865051544Z at decodeURIComponent (<anonymous>)
pigallery2-1 | 2026-11-03T12:29:47.865052252Z at decode_param (/app/node_modules/express/lib/router/layer.js:172:12)
pigallery2-1 | 2026-11-03T12:29:47.865052877Z at Layer.match (/app/node_modules/express/lib/router/layer.js:148:15)
pigallery2-1 | 2026-11-03T12:29:47.865053419Z at matchLayer (/app/node_modules/express/lib/router/index.js:585:18)
pigallery2-1 | 2026-11-03T12:29:47.865053961Z at next (/app/node_modules/express/lib/router/index.js:226:15)
pigallery2-1 | 2026-11-03T12:29:47.865054502Z at addTPl (/app/src/backend/routes/PublicRouter.js:72:20)
pigallery2-1 | 2026-11-03T12:29:47.865065169Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.865065877Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.865066502Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.865072711Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12)
pigallery2-1 | 2026-11-03T12:29:47.865073294Z at next (/app/node_modules/express/lib/router/index.js:280:10)
pigallery2-1 | 2026-11-03T12:29:47.865073794Z at injectAppVersion (/app/src/backend/middlewares/VersionMWs.js:18:13)
pigallery2-1 | 2026-11-03T12:29:47.865074336Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.865075211Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.865075752Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.865076294Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12) {
pigallery2-1 | 2026-11-03T12:29:47.865076836Z statusCode: 400,
pigallery2-1 | 2026-11-03T12:29:47.865077294Z status: 400
pigallery2-1 | 2026-11-03T12:29:47.865077794Z }
pigallery2-1 | 2026-11-03T12:29:47.865097252Z 11/3/2026, 12:29:47 PM[ERROR] ErrorDTO {
pigallery2-1 | 2026-11-03T12:29:47.865098211Z code: 36,
pigallery2-1 | 2026-11-03T12:29:47.865098752Z message: 'Unknown server side error',
pigallery2-1 | 2026-11-03T12:29:47.865099294Z details: URIError: Failed to decode param '%c0'
pigallery2-1 | 2026-11-03T12:29:47.865099836Z at decodeURIComponent (<anonymous>)
pigallery2-1 | 2026-11-03T12:29:47.865100336Z at decode_param (/app/node_modules/express/lib/router/layer.js:172:12)
pigallery2-1 | 2026-11-03T12:29:47.865100877Z at Layer.match (/app/node_modules/express/lib/router/layer.js:148:15)
pigallery2-1 | 2026-11-03T12:29:47.865104752Z at matchLayer (/app/node_modules/express/lib/router/index.js:585:18)
pigallery2-1 | 2026-11-03T12:29:47.865105294Z at next (/app/node_modules/express/lib/router/index.js:226:15)
pigallery2-1 | 2026-11-03T12:29:47.865105836Z at addTPl (/app/src/backend/routes/PublicRouter.js:72:20)
pigallery2-1 | 2026-11-03T12:29:47.865106419Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.865106961Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.865107502Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.865108002Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12)
pigallery2-1 | 2026-11-03T12:29:47.865108544Z at next (/app/node_modules/express/lib/router/index.js:280:10)
pigallery2-1 | 2026-11-03T12:29:47.865109044Z at injectAppVersion (/app/src/backend/middlewares/VersionMWs.js:18:13)
pigallery2-1 | 2026-11-03T12:29:47.865109586Z at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
pigallery2-1 | 2026-11-03T12:29:47.865111586Z at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)
pigallery2-1 | 2026-11-03T12:29:47.865112127Z at /app/node_modules/express/lib/router/index.js:286:9
pigallery2-1 | 2026-11-03T12:29:47.865112669Z at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12) {
pigallery2-1 | 2026-11-03T12:29:47.865113211Z statusCode: 400,
pigallery2-1 | 2026-11-03T12:29:47.865113711Z status: 400
pigallery2-1 | 2026-11-03T12:29:47.865114211Z },
pigallery2-1 | 2026-11-03T12:29:47.865114711Z request: { method: 'GET', url: '/%c0' }
pigallery2-1 | 2026-11-03T12:29:47.865115211Z }