Open Source · MIT
Glint

Glint — A Self-Hosted Photography Portfolio

An open-source portfolio that any photographer can put online with one command, on a Raspberry Pi, a Mac mini or a $5 cloud server. It runs my own photography at glint.dheepstack.com.

Open SourceNext.js 16SQLiteImage PipelineDocker Multi-ArchCloudflare TunnelRaspberry Pi

The Problem

Photographers who want to show their work online have two kinds of options. Hosted platforms own the audience, compress the images, and wrap the work in feeds and ads.

Self-hosted tools like Immich and PhotoPrism are excellent, but they're built to manage a photo library: face recognition, search, deduplication. Putting them online as a public portfolio means databases, reverse proxies and an afternoon of configuration.

The Idea

Build the opposite: a tool that does one thing, showing your best work beautifully, and is genuinely zero-config. One command, no settings to edit, one folder to back up.

Glint keeps the photos in charge: a near-black canvas, native aspect ratios, and exactly one deliberate animation, the shared-element morph from grid to fullscreen.

How It Works for Photographers

Run One Command

docker compose up -d pulls a ready-made image. Add a Cloudflare Tunnel token and your Glint portfolio is on the internet over HTTPS, with no ports opened on the router.

Drag In Your Photos

Create albums, drop in hundreds of photos at once, reorder by dragging. Albums can be public, unlisted, or password-protected for client sets.

Share a Real Portfolio

Your name, an About page with contact links, EXIF details on every shot, anonymous likes, and link previews that show your cover photo wherever you share it.

Architecture: The Life of a Photo

Glint is a single Next.js 16 application, with the public site, admin panel and API in one process, backed by SQLite and the local disk. Here's what happens to a photo from the moment it's dropped into the uploader to the moment someone shares its link.

1

Upload

Hundreds of photos dropped in → One request per photo

The admin uploader streams photos one at a time with live progress. Each request stays under Cloudflare's 100 MB body limit, a Raspberry Pi only ever processes one image at once, and a single bad file never sinks the batch.

Multipart uploadsSequential queuePer-file error reporting
2

Process

Original camera file → Archive + 2 web variants

The original is kept byte-for-byte as the photographer's archive. sharp bakes in EXIF orientation and writes a 400px grid thumbnail and a 1600px lightbox image. Re-encoding strips every byte of metadata, including GPS.

sharp (libvips)EXIF orientationexifrMetadata stripping
3

Store

Processed files → One portable folder

Albums, photos, likes and settings live in a single SQLite file next to the uploads. There's no database server to run, and the whole site backs up by copying one folder. Schema migrations apply automatically when the container starts.

SQLitelibSQLDrizzle ORMAuto-migrations
4

Guard

Request for an image → Access-checked response

Every image goes through one access check: public, unlocked with the album password, or admin. Originals are admin-only, locked albums are never cached at the CDN, and changing an album's password instantly revokes old unlocks.

Single access policySigned session cookiesCache-Control: privatePer-IP rate limits
5

Render

Mixed aspect ratios → Justified contact-sheet rows

A hand-rolled justified-layout algorithm packs photos into full-width rows at their native aspect ratios, with no crops and no third-party layout library. Opening a photo morphs it from its spot in the grid into the fullscreen lightbox.

Justified layoutResizeObserverFramer Motion layoutIdReduced-motion fallback
6

Share

An album link → A rich preview anywhere

Every album renders its own Open Graph card with its cover photo, so links look right in iMessage, Slack and X. A live sitemap lists only public albums, and unlisted or locked ones are kept out of search.

Open Graph / X cardsDynamic sitemap.xmlrobots.txtnoindex for private albums

Engineering Highlights

Secure by Default, Not by Documentation

The people who self-host a portfolio won't edit config files, so nothing ships with a default password or secret. On first start Glint generates its session secret and an admin password, saves them in the data folder, and prints the password once in the logs.

Location Privacy Built In

Camera files often carry GPS coordinates, sometimes of the photographer's home. Visitors only ever receive re-encoded copies with metadata stripped. The untouched originals stay on the server, reachable only by the admin.

One Container, One Folder

No database server, no object storage, no queue. The app, the SQLite database and the image pipeline run in one container, and all state lives in a single mounted folder. The whole setup is understandable in a single read.

Runs Safely on Any Linux Box

Docker creates a missing data folder owned by root. A tiny entrypoint fixes ownership on first start, then drops root with setpriv before the server runs, so a fresh install just works and the app never runs as root.

Open Source, Built to Be Run by Anyone

Glint is MIT-licensed on GitHub. The goal isn't a hosted service; it's software any photographer can own. So the distribution matters as much as the app: nobody should need to compile anything.

Multi-Arch Release Pipeline

Tagging a version triggers GitHub Actions to build the image natively on both x86 and ARM runners in parallel (no QEMU emulation), then stitch them into one multi-arch manifest on GHCR. A Raspberry Pi and a cloud VM pull the same tag and get the right binary automatically.

One Compose File, Two Ways Online

Compose profiles switch the edge without changing the app: --profile tunnel adds Cloudflare Tunnel for home hardware, and --profile caddyadds automatic HTTPS for a cloud server. Both wait for Glint's health check before taking traffic.

Safe for a Public Repo

My other homelab apps deploy through self-hosted GitHub runners. Glint deliberately doesn't: on a public repository, a pull request could run code on the home server. Instead, my Pi pulls tagged releases exactly like any other user.

Dogfooded on a Raspberry Pi

glint.dheepstack.com runs the public compose file on the Raspberry Pi in my homelab, with photos on its NVMe SSD, behind the same Cloudflare Tunnel as my other self-hosted apps. If a release breaks, I'm the first to know.

Self-host it in three commands

curl -fsSLO https://raw.githubusercontent.com/dheepak875/Glint/main/docker-compose.yml
docker compose up -d
docker compose logs glint   # your generated admin password

Tech Stack

App

Next.js 16 (App Router)React 19TypeScriptzodFramer Motion

Data & media

SQLite via libSQLDrizzle ORMsharpexifriron-session

Delivery

Docker (standalone build)GitHub ActionsGHCR multi-arch imagesCloudflare TunnelCaddy

What's Next

The next big step is a serverless option for photographers who don't want any hardware: Cloudflare Workers for the app, R2 for photos (no bandwidth bills for a photo site) and D1 for the database, at roughly $5 a month. Getting there means swappable storage, in-browser image resizing and WebCrypto-based password hashing, which will also make the self-hosted version lighter on a Pi.

See It With Real Photos

My own photography, served by Glint from a Raspberry Pi in my homelab. Open a photo to see the lightbox and its EXIF details.

Visit glint.dheepstack.com ↗
Star it on GitHub ↗